這個版本的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. 注意區分signal
和broadcast
:”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