线程同步之条件变量使用手记
由来:
最近一直在想怎么高效率的在IO线程接收到数据时通知逻辑线程(基于线程池)工作的问题,像网络编程的服务器模型的一些模型都需要用到这个实现,下面我这里简单的罗列一个多线程的网络服务器模型
半同步/半异步(half-sync/half-async):
许多餐厅使用 半同步/半异步 模式的变体。例如,餐厅常常雇佣一个领班负责迎接顾客,并在餐厅繁忙时留意给顾客安排桌位,为等待就餐的顾客按序排队是必要的。领班由所有顾客“共享”,不能被任何特定顾客占用太多时间。当顾客在一张桌子入坐后,有一个侍应生专门为这张桌子服务。
对于上面罗列的这种模型,本文讨论的问题是当领班接到客人时,如何高效率的通知侍应生去服务顾客.
在我们使用很广泛的线程池实现中,也会有一样的问题
方法实现:
1.使用锁+轮询
使用这种方法可以很简单的实现,但是会有一定的性能消耗,其还有一个点要好好把握,就是一次轮询没有结果后相隔多久进行下一次的轮询,间隔时间太短,消耗的CPU资源较多,间隔时间太长,不能很及时的响应请求。这就相当于上面的这个例子,侍应生时不时的取询问领班有没有顾客到来
2.使用条件变量的线程同步
线程条件变量pthread_cond_t
线程等待某个条件
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
通知函数
通知所有的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
只通知一个线程
int pthread_cond_signal(pthread_cond_t *cond);
正确的使用方法
pthread_cond_wait用法:
pthread_mutex_lock(&mutex);
while(condition_is_false)
{
pthread_cond_wait(&cond,&mutex);
}
condition_is_false=true; //此操作是带锁的,也就是说只有一个线程同时进入这块
pthread_mutex_unlock(&mutex);
pthread_cond_signal用法:
pthread_mutex_lock(&mutex);
condition_is_false=false;
pthread_cond_signal(&cond)
pthread_mutex_unlock(&mutex)
我刚初用的时候,觉得非常的奇怪,为什么要这样用,加了mutex后还需要一个condition_is_false变量来表示有没有活干。其实这样子的一个操作主要是为了解决“假激活”问题,因为我么您这里的使用场景,只需要激活一个线程,因为一个线程干一个活,而不是多个线程干一个活,所以为了避免线程被激活了,但实际又没有事情干,所以使用了这么一套机制。
实际上,信号和pthread_cond_broadcast是两个常见的导致假唤醒的情况。假如条件变量上有多个线程在等待,pthread_cond_broadcast会唤醒所有的等待线程,而pthread_cond_signal只会唤醒其中一个等待线程。这样,pthread_cond_broadcast的情况也许要在pthread_cond_wait前使用while循环来检查条件变量。
来个例子:
1 #include <pthread.h>
2 #include <stdio.h>
3 #include<stdlib.h>
4 #include<unistd.h>
5
6 /* For safe condition variable usage, must use a boolean predicate and */
7 /* a mutex with the condition. */
8 int workToDo = 0;
9 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
10 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
11
12 #define NTHREADS 20
13
14 static void checkResults(char *string, int rc) {
15 if (rc) {
16 printf("Error on : %s, rc=%d",
17 string, rc);
18 exit(EXIT_FAILURE);
19 }
20 return;
21 }
22
23 void *threadfunc(void *parm)
24 {
25 int rc;
26
27 while (1) {
28 /* Usually worker threads will loop on these operations */
29 rc = pthread_mutex_lock(&mutex);
30 checkResults("pthread_mutex_lock()\n", rc);
31
32 while (!workToDo) {
33 printf("Thread blocked\n");
34 rc = pthread_cond_wait(&cond, &mutex);
35 checkResults("pthread_cond_wait()\n", rc);
36 }
37 printf("Thread awake, finish work!\n");
38 sleep(2);
39 /* Under protection of the lock, complete or remove the work */
40 /* from whatever worker queue we have. Here it is simply a flag */
41 workToDo = 0;
42 printf("In mutex lock\n");
43 rc = pthread_mutex_unlock(&mutex);
44 sleep(2);
45 printf("Out mutex lock\n");
46 checkResults("pthread_mutex_lock()\n", rc);
47 }
48 return NULL;
49 }
50
51 int main(int argc, char **argv)
52 {
53 int rc=0;
54 int i;
55 pthread_t threadid[NTHREADS];
56
57 printf("Enter Testcase - %s\n", argv[0]);
58
59 printf("Create %d threads\n", NTHREADS);
60 for(i=0; i<NTHREADS; ++i) {
61 rc = pthread_create(&threadid[i], NULL, threadfunc, NULL);
62 checkResults("pthread_create()\n", rc);
63 }
64
65 sleep(5); /* Sleep is not a very robust way to serialize threads */
66
67 for(i=0; i<5; ++i) {
68 printf("Wake up a worker, work to do...\n");
69
70 rc = pthread_mutex_lock(&mutex);
71 checkResults("pthread_mutex_lock()\n", rc);
72
73 /* In the real world, all the threads might be busy, and */
74 /* we would add work to a queue instead of simply using a flag */
75 /* In that case the boolean predicate might be some boolean */
76 /* statement like: if (the-queue-contains-work) */
77 if (workToDo) {
78 printf("Work already present, likely threads are busy\n");
79 }
80 workToDo = 1;
81 rc = pthread_cond_broadcast(&cond);
82 // rc = pthread_cond_signal(&cond);
83 checkResults("pthread_cond_broadcast()\n", rc);
84
85 rc = pthread_mutex_unlock(&mutex);
86 checkResults("pthread_mutex_unlock()\n", rc);
87 sleep(5); /* Sleep is not a very robust way to serialize threads */
88 }
89
90 printf("Main completed\n");
91 exit(0);
92 return 0;
93 }
事实上上面的例子无论是使用pthread_cond_signal还是pthread_cond_broadcast,都只会打印Thread awake, finish work5次,大家可能会觉得非常奇怪,但是实际情况就是这样的。 为了明白其pthread_cont_wait内部干了什么工作,有必要深入一下其内部实现。
关于其内部实现伪代码如下:
1 pthread_cond_wait(mutex, cond): 2 value = cond->value; /* 1 */ 3 pthread_mutex_unlock(mutex); /* 2 */ 4 pthread_mutex_lock(cond->mutex); /* 10 */ pthread_cond_t自带一个mutex来互斥对waiter等待链表的操作 5 if (value == cond->value) { /* 11 */ 检查一次是不是cond有被其他线程设置过,相当于单例模式的第二次检测是否为NULL 6 me->next_cond = cond->waiter; 7 cond->waiter = me;//链表操作 8 pthread_mutex_unlock(cond->mutex); 9 unable_to_run(me); 10 } else 11 pthread_mutex_unlock(cond->mutex); /* 12 */ 12 pthread_mutex_lock(mutex); /* 13 */ 13 14 pthread_cond_signal(cond): 15 pthread_mutex_lock(cond->mutex); /* 3 */ 16 cond->value++; /* 4 */ 17 if (cond->waiter) { /* 5 */ 18 sleeper = cond->waiter; /* 6 */ 19 cond->waiter = sleeper->next_cond; /* 7 */ //链表操作 20 able_to_run(sleeper); /* 8 */ 运行sleep的线程,即上面的me 21 } 22 pthread_mutex_unlock(cond->mutex); /* 9 */
pthread_cond_broadcast虽然能够激活所有的线程,但是激活之后会有mutex锁,也就是说他的激活是顺序进行的,只有第一个激活的线程调用pthread_mutex_unlock(&mutex)后,后一个等待的线程才会继续运行.因为从pthread_cond_wait(&cond,&mutex)到pthread_mutex_unlock(&mutex)区间是加的独占锁,从wait激活后的第一个线程占用了这个锁,所以其他的线程不能运行,只能等待。所以当第一个被激活的线程修改了condition_is_false后(上面测试代码的workToDo),接着调用pthread_mutex_unlock(&mutex)后,此时其他等待在cond的一个线程会激活,但是此时condition_is_false已经被设置,所以他跑不出while循环,当调用pthread_cond_wait时,其内部pthread_mutex_unlock(mutex)调用会导致另一个在它后面的等待在cond的线程被激活。
所以,通过这种方式,即便是误调用了pthread_cond_broadcast或者由于信号中断的原因激活了所有在等待条件的线程,也能保证其结果是正确的。
另外说一句题外话,很多人写的基于条件变量线程同步的框架,说自己是无锁的,其实这是不对的,只是内部锁的机制在pthread_cond_wait实现了而已,其还是基于互斥锁的实现。真正想要达到无锁的可以关注一下lockfree相关的CAS算法,其内部使用一个intel CPU的cmpxchg8指令完成的,其实这种实现个人认为和传统锁相比只是一个非阻塞锁和阻塞锁的区别。