一起來寫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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章