一起来写web server 08 -- 多线程+非阻塞IO+epoll

一起来写web server 08 -- 多线程+非阻塞IO+epoll


到了多线程,一些东西就变得耐人寻味了.

这个版本是在前面单线程epoll的基础上引入了线程池,当然不是前面玩具一样的线程池,而是一个通用的组件,生产者消费者队列.

生产者消费者队列

生产者消费者问题是操作系统中一个很经典的同步互斥问题,已经有了很不错的解决方案,将它的解决方案拓展一下,就可以用于我们的实践啦.

我自己写了一个生产者消费者的队列,然后发现muduo中已经内置了这种模型,而且使用起来比我写的更加顺手,所以我就引用它的实现,我这里稍微来讲解一下它的实现,然后我会顺带讲解一下我的思路.

muduo库的生产者消费者模型

这是ThreadPool类的一个声明:

class ThreadPool : noncopyable
{
public:
    typedef boost::function<void()> Task; /* 需要执行的任务 */
private:
    bool isFull();
    Task take();
    size_t queueSize();
    int threadNum_; /* 线程的数目 */
    int maxQueueSize_;
    std::list<Task> queue_; /* 工作队列 */
    MutexLock mutex_;
    Condition notEmpty_;
    Condition notFull_;
};

这里的boost::function其实在cpp 11标准中已经加入了,你如果没有安装boost库的话,可以缓存std的版本,效果是一样的.因为boost本来就是cppstd的一个备选库.

为什么要使用boost::function不用我多说,你可以查看这里:http://blog.csdn.net/solstice/article/details/3066268

我们来看一下代码的实现,首先是构造函数:

ThreadPool::ThreadPool(int threadNum, int maxQueueSize)
    : threadNum_(threadNum)
    , maxQueueSize_(maxQueueSize)
    , mutex_()
    , notEmpty_(mutex_)
    , notFull_(mutex_)

{
    assert(threadNum >= 1 && maxQueueSize >= 1);
    /* 接下来要构建threadNum个线程 */
    pthread_t tid_t;
    for (int i = 0; i < threadNum; i++) {
        Pthread_create(&tid_t, NULL, startThread, this);
    }
}

这里ThreadPool有两个条件变量,一个是notEmpty_,一个是notFull_,构造函数接受两个参数,一个是线程的数目,一个是最大的队列的大小.

接下来是所有的线程都运行的函数startThread:

void* ThreadPool::startThread(void* obj)
{ /* 工作者线程 */
    Pthread_detach(Pthread_self());
    ThreadPool* pool = static_cast<ThreadPool*>(obj);
    pool->run();
    return pool;
}

它们都开始调用run函数:

void ThreadPool::run()
{
    for ( ; ; ) { /* 一直运行下去 */
        Task task(take());
        if (task) {
            //mylog("task run!");
            task();
        }
        //mylog("task over!");
    }
}

run函数非常简单,就是不断从队列中取出任务,然后运行任务,没有任务的话,会阻塞在那里.

我们来看take函数:

ThreadPool::Task ThreadPool::take()
{
    MutexLockGuard lock(mutex_); /* 加锁 */
    while (queue_.empty()) { /* 如果队列为空 */
        notEmpty_.wait(); /* 等待 */
    }
    Task task;
    if (!queue_.empty()) {
        task = queue_.front();
        queue_.pop_front();
        if (maxQueueSize_ > 0) { /* 通知生产者队列有空位置了 */
            notFull_.notify();
        }
    }
    //mylog("threadpool take 1 task!");
    return task;
}

对于生产者而言,有一个非常重要的函数,那就是append:

bool ThreadPool::append(Task&& task)
{ /* 使用了右值引用 */
    {
        MutexLockGuard lock(mutex_); /* 首先加锁 */
        while (isFull()) { /* 如果队列已满 */
            notFull_.wait(); /* 等待queue有空闲位置 */
        }
        assert(!isFull());
        queue_.push_back(std::move(task)); /* 直接用move语义,提高了效率 */
        //mylog("put task onto queue!");
    }
    notEmpty_.notify(); /* 通知消费者有任务可做了 */
}

生产者消费者队列的代码就是这么简单,但是muduo库写的确实很漂亮.

我的思路

其实代码基本上和前面的类似,不同的是,我压根就没有考虑过使用boost::funcitonboost::bind这对神器,因为我之前也压根就没有这样编过码.

如果不用boost::funcitonboost::bind这两样东西,我们要实现类似的代码的话,可能的一个解决方案是使用模版(template).

队列里面放的是T类型,然后消费者取出一个T类型,调用T类型的一个run或者别的什么不带参数的方法.这样以来,对T类型就有了限制,要求T类型必须实现run之类的方法.

而且代码变得不太容易读.加了模版的玩意总是不容易读,不是吗?所以要积极使用cpp的新特性.

主程序变成了生产者

这一次的代码变得简洁多了,

int main(int argc, char *argv[])
{
    int listenfd = Open_listenfd(8080); /* 8080号端口监听 */
    epoll_event events[MAXEVENTNUM];
    sockaddr clnaddr;
    socklen_t clnlen = sizeof(clnaddr);

    block_sigpipe(); /* 首先要将SIGPIPE消息阻塞掉 */

    int epollfd = Epoll_create(1024); /* 10基本上没有什么用处 */
    addfd(epollfd, listenfd, false); /* epollfd要监听listenfd上的可读事件 */
    ThreadPool pools(10, 30000); /* 10个线程,300个任务 */
    HttpHandle::setEpollfd(epollfd);
    HttpHandle handle[2000];

    for ( ; ;) {
        int eventnum = Epoll_wait(epollfd, events, MAXEVENTNUM, -1);
        for (int i = 0; i < eventnum; ++i) {
            int sockfd = events[i].data.fd;
            if (sockfd == listenfd) { /* 有连接到来 */
                //mylog("connection comes!");
                for ( ; ; ) {
                    int connfd = accept(listenfd, &clnaddr, &clnlen);
                    if (connfd == -1) {
                        if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { /* 将连接已经建立完了 */
                            break;
                        }
                        unix_error("accept error");
                    }
                    handle[connfd].init(connfd); /* 初始化 */
                    addfd(epollfd, connfd, false); /* 加入监听 */
                }
            }
            else { /* 有数据可读或者可写 */
                pools.append(boost::bind(&HttpHandle::process, &handle[sockfd]));
            }
        }
    }
    return 0;
}

注意最后的一句boost::bind(&HttpHandle::process, &handle[sockfd]),直接将对象往函数上一绑定,就往队列里面扔.非常爽.

这一次,我们终于将SIGPIPE消息给忽略掉了,主要是调用下面这个函数:

void block_sigpipe()
{
    sigset_t signal_mask;
    sigemptyset(&signal_mask);
    sigaddset(&signal_mask, SIGPIPE);
    int rc = pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
    if (rc != 0) {
        printf("block sigpipe error\n");
    }
}

shared_ptr并不是线程安全的

正如文章开头所讲的,多线程一来,很多事情就变得莫名奇妙了,比如说shared_ptr,因为这个玩意的线程不安全性,我调了半天bug,才发现原来是Cache的查找函数出了问题,下面是修改过后的线程安全版本的函数:

/* 线程安全版本的getFileAddr */
    void getFileAddr(std::string fileName, int fileSize, boost::shared_ptr<FileInfo>& ptr) {
        /*-
        * shared_ptr并不是线程安全的,对其的读写都需要加锁.
        */
        MutexLockGuard lock(mutex_);
        if (cache_.end() != cache_.find(fileName)) { /* 如果在cache中找到了 */
            ptr = cache_[fileName];
            return;
        }
        if (cache_.size() >= MAX_CACHE_SIZE) { /* 文件数目过多,需要删除一个元素 */
            cache_.erase(cache_.begin()); /* 直接移除掉最前一个元素 */
        }
        boost::shared_ptr<FileInfo> fileInfo(new FileInfo(fileName, fileSize));
        cache_[fileName] = fileInfo;
        ptr = std::move(fileInfo); /* 直接使用move语义 */
    }

至于为什么不安全,可以查看这里,写的再好不过了:http://blog.csdn.net/solstice/article/details/8547547

多线程的调试

原谅我到了这接近尾声的时候,才提起多线程的调试,首先要说一句的是,多线程真的不太好调,因为很难重现错误,但是我在这里稍稍介绍一下我的技巧.

打印

打印算是屡试不爽的一种方法,对于我们这个简陋的web server,我封装了一个日志函数mylog:

void mlog(pthread_t tid, const char *fileName, int lineNum, const char *func, const char *log_str, ...)
{
    va_list vArgList; //定义一个va_list型的变量,这个变量是指向参数的指针.
    char buf[1024];
    va_start(vArgList, log_str); //用va_start宏初始化变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数
    vsnprintf(buf, 1024, log_str, vArgList); //注意,不要漏掉前面的_
    va_end(vArgList);  //用va_end宏结束可变参数的获取
    printf("%lu:%s:%d:%s --> %s\n", tid, fileName, lineNum, func, buf);
}

然后定义了一个宏,方便使用这个函数:

#define mylog(formatPM, args...)\
  mlog(pthread_self(), __FILE__, __LINE__, __FUNCTION__,  (formatPM) , ##args)

需要日志的时候,可以像printf函数一样使用:

mylog("My simple web server! %d, %s\n", 1, "hello, workd!");

这个宏展开后会调用mlog函数,打印出行,文件名,函数名等信息,对付我们这个小玩意足够了.

用VS来调试

VS其实也内置了线程的调试,你可以结合Visual Gdb一起来调试linux下的代码.一两个线程问题倒是不大,不过线程多了的话,这个玩意就不好调了,要我说,最好的方法还是分析日志.

总结

这个版本已经算是比较强劲的一个版本了,修复了前面的一些bug,但是引入了新的bug,这个bug我也是折腾了很久才弄出来.

一般在单线程下不可能出现这样的bug,只有在多线程的条件下,这样的代码才变成了bug,正如前面见到的,每个HttpHandle处理一个连接,试想这样一种情形:客户端不知道因为什么原因,第一次发送了这样的数据:

GET /

隔了很短时间才会发送余下的数据.这时,第一次发送的数据正在被另外一个线程处理,在多线程条件下,对于第二次到来的数据,这个HttpHandle会交由另外一个线程处理,也就是说,有两个线程在不加锁地使用同一个HttpHandle,不出问题才怪.

解决方案是有的,那就是EPOLLONESHOT参数.不过那是下一个版本的故事啦.

和之前类似的,代码在这里:https://github.com/lishuhuakai/Spweb

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 这次的这个版本相对于前面的第6个版本有些许加强,那就是将epoll由LT模式变成了ET模式. 对于采用了LT工作模...
    Yihulee阅读 404评论 0 0
  • 从哪说起呢? 单纯讲多线程编程真的不知道从哪下嘴。。 不如我直接引用一个最简单的问题,以这个作为切入点好了 在ma...
    Mr_Baymax阅读 2,726评论 1 17
  • Java-Review-Note——4.多线程 标签: JavaStudy PS:本来是分开三篇的,后来想想还是整...
    coder_pig阅读 1,629评论 2 17
  • 高三一个神奇的阶段,听别人说高三会很累,但是到了现在,高三已经过了一半,我不知道我算不算努力,每天与舍友5点40起...
    寂寞的陌生人阅读 160评论 0 0
  • 放爱一条生路 让它安全软着陆 不要再执着地缠绕 洒脱不羁的风筝 只会懂得牵引线是枷锁 放爱一条生路 施爱的程序和技...
    瓶水之冰阅读 182评论 0 2