一起来写web server 05 -- 多线程进阶版本


这个版本的web server比第4版稍微做了一点改进,那就是由主线程统一接收连接,然后连接的处理由子线程来完成.因此,这里就引入了条件变量以及同步互斥的问题.

同步机制

muduo库中有一个关于同步机制的封装,我这里就直接采用了.我这里来介绍一下这个封装吧.

下面是Conditon这个类的代码:

class Condition : noncopyable
{
    private:
        MutexLock& mutex_; /* 之前的锁的一个引用 */
        pthread_cond_t pcond_; /* 系统定义的条件变量的类型 */
        ... ...
}

这个类的构造函数用于初始化同步变量:

explicit Condition(MutexLock& mutex)
        : mutex_(mutex)
    {
        pthread_cond_init(&pcond_, NULL); /* 初始化同步变量 */
    }

析构函数就销毁掉同步变量:

~Condition()
    {
        pthread_cond_destroy(&pcond_); /* 销毁条件变量 */
    }

等待某个条件:

void wait()
    {
        MutexLock::UnassignGuard ug(mutex_);
        pthread_cond_wait(&pcond_, mutex_.getPthreadMutex()); /* 等待Mutex */
    }

通知单个线程:

void notify()
    {
        pthread_cond_signal(&pcond_); /* 唤醒一个线程 */
    }

条件变量只有一种正确的使用方式,几乎不可能用错,对于wait端:

  1. 必须与mutex一起使用,该布尔表达式的读写需受此mutex保护.
  2. mutex已经上锁的时候才能调用wait().
  3. 把判断布尔条件和wait()放到while循环中.
    写成代码是这个样子的:
MutexLock mutex;
Condition cond(mutex);
std::deque<int> queue;

int dequeue() {
    MutexLockGuard lock(mutex); /* 加锁 */
    while (queue.empty()) {
        cond.wait(); 
    }
    assert(!queue.empty());
    int top = queue.front();
    queue.pop_front();
    return top;
}

对于sinal/broadcast端:

  1. 不一定要在mutex已经上锁的情况下调用signal(理论上).
  2. signal之前一般要修改布尔表达式.
  3. 修改布尔表达式通常要用mutex保护.
  4. 注意区分signalbroadcast:"broadcast"通常用于表明状态变化,而signal表示资源可用.
    写成代码是:
void enqueue(int x) 
{
    MutexLockGuard lock(mutex); // 加锁
    queue.push_back(x);
    cond.signal(); // 可以移出临界区之外
}

以上引自linux多线程服务端编程.

我来谈一下我的理解:

cond中之所以需要mutex,是因为在执行到

while (condition) {
 cond.wait();
}

时,需要将cond中持有的mutex解锁.一旦接收到signal,它需要重新抢夺这个mutex,抢到了,才能从wait函数中返回.

为什么cond.wait()要放入while循环中呢?一方面是因为spurious wakeup,之所以会有这个东西,是速度的考量,一般来说,即使没有spurious wakeup,你也要这么写代码,举个栗子.

在生产者消费者模型之中,消费者1获得锁,发现queue为空,wait,消费者2获得锁,发现queue为空,wait,生产者3获得锁,将生产的产品放入queue,调用signal,并且释放了mutex,t1,t2被唤醒,可以预见的是,这两者只会有一个获得锁,消费完这个产品,然后另一个获得锁,发现为空,还是得继续等待,这就是while的由来,当然,至于signal为什么会唤醒多个线程,man手册上就是这么说的.

我们的代码

```cpp
/*-
* 线程池的加强版本.主要是主线程统一接收连接,其余都是工作者线程,这里的布局非常类似于一个生产者.
* 多个消费者.
*/

#define MAXNCLI 100

MutexLock mutex; /* 全局的锁 */
Condition cond(mutex); /* 全局的条件变量 */
int clifd[MAXNCLI], iget, iput;

int main(int argc, char *argv[])
{
    int listenfd = Open_listenfd(8080); /* 8080号端口监听 */
    signal(SIGPIPE, SIG_IGN);
    pthread_t tids[10];
    void* thread_main(void *);

    for (int i = 0; i < 10; ++i) {
        int *arg = (int *)Malloc(sizeof(int));
        *arg = i;
        Pthread_create(&tids[i], NULL, thread_main, (void *)arg);
    }
    struct sockaddr cliaddr; /* 用于存储对方的ip信息 */
    socklen_t clilen;
    for (; ; ) {
        int connfd = Accept(listenfd, &cliaddr, &clilen);
        {
            MutexLockGuard lock(mutex); /* 加锁 */
            clifd[iput] = connfd; /* 涉及到对共享变量的修改,要加锁 */
            if (++iput == MAXNCLI) iput = 0;
            if (iput == iget) unix_error("clifd is not big enough!\n");
        }
        cond.notify(); /* 通知一个线程有数据啦! */
    }
    return 0;
}

线程的代码是这样的:

void*
thread_main(void *arg)
{
    int connfd;
    printf("thread %d starting\n", *(int *)arg);
    Free(arg);
    for ( ; ;) {
        {
            MutexLockGuard lock(mutex); /* 加锁 */
            while (iget == iput) { /* 没有新的连接到来 */
                /*-
                * 代码必须用while循环来等待条件变量,原因是spurious wakeup
                */
                cond.wait(); /* 这一步会原子地unlock mutex并进入等待,wait执行完毕会自动重新加锁 */
            }
            connfd = clifd[iget]; /* 获得连接套接字 */
            if (++iget == MAXNCLI) iget = 0;
        }
        doit(connfd);
        close(connfd);
    }
}

总结

这个版本在原来的版本上增加了同步互斥操作,在某种程度上增加了难度.

具体代码还是看这里吧!:https://github.com/lishuhuakai/Spweb

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

推荐阅读更多精彩内容