多线程引入同步互斥机制,就是为了在某一时刻只能有一个线程可以实现对共享资源的访问。不论互斥锁还是信号量其本质都是一致的。条件变量的工作原理很简单,即让当前不需要访问共享资源的线程进行阻塞等待(睡眠),如果某一时刻共享资源的状态改变需要某一个线程处理,那么则可以通知该线程进行处理(唤醒)。
条件变量可以看成是互斥锁的补充,因为条件变量需要结合互斥锁一起使用,之所以这样,是因为互斥锁的状态只有锁定和非锁定两种状态,无法决定线程执行先后,有一定的局限。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。关于条件变量如何配合使用,本笔记从示例中详细分析。
条件变量的使用同样需要初始化,其核心操作为阻塞线程及唤醒线程,最后将其摧毁。
#include <pthread.h> int pthread_cond_destroy(pthread_cond_t *cond); int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);点击复制复制失败已复制
pthread_cond_init()
函数的功能是初始化条件变量。参数 cond
表示条件变量的标志符。参数 attr
用来设置条件变量的属性,通常为 NULL
,执行默认属性。如果执行成功则会将条件变量的标志符保存在参数 cond
中, pthread_cond_destroy()
函数表示摧毁一个条件变量。
#include <pthread.h> int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);点击复制复制失败已复制
pthread_cond_signal()
函数的功能是发送信号给至少一个处于阻塞等待的线程,使其脱离阻塞状态,继续执行。如果没有线程处于阻塞等待状态, pthread_cond_signal()
函数也会成功返回。 pthread_cond_broadcast()
函数的功能是唤醒当前条件变量所指定的所有阻塞等待的线程。上述两个函数中, pthread_cond_signal()
函数使用的频率更高。按照互斥锁对共享资源保护规则,条件变量 cond
也作为一种共享资源,则 pthread_cond_signal()
函数即可以放在 pthread_mutex_lock()
函数和 pthread_mutex_unlock()
函数之间,也可以采用另一种写法,将 pthread_cond_signal()
函数放在 pthread_mutex_lock()
函数和 pthread_mutex_unlock()
函数之后,当然有时也可以不添加加锁解锁操作。这些都要视环境而定,后续将通过代码示例分析。
#include <pthread.h> 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);点击复制复制失败已复制
pthread_cond_wait()
函数用于使线程进入睡眠状态,当使用 pthread_cond_wait()
函数使线程进入阻塞状态时,必须先对其进行加锁操作,之后再进行解锁操作。通俗的说,即 pthread_cond_wait()
函数必须放在 pthread_mutex_lock()
函数和 pthread_mutex_unlock()
函数之间。参数 cond
为条件变量的标志符,参数 mutex
则为互斥锁的标志符。值得注意的是,也是函数的重点是 pthread_cond_wait()
函数一旦实现阻塞,使线程进入睡眠之后,函数自身会将之前的线程已经持有的互斥锁自动释放。不同于唤醒操作,睡眠操作必须要进行加锁处理。
条件变量的使用,比互斥锁、信号量更复杂一点。下面将使用条件变量,对信号量的使用中的代码继续修改,最终的目的仍然是实现按照顺序线程1输入、线程2读取、线程3读取。
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <string.h> #define N 32 #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) char buf[N] = ""; pthread_cond_t cond; pthread_mutex_t lock; void *thread1_handler(void *arg) { while (1) { fgets(buf, N, stdin); buf[strlen(buf) - 1] = '\0'; pthread_cond_signal(&cond); } pthread_exit("thread1...exit"); } void *thread2_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); // 执行加锁操作 pthread_cond_wait(&cond,&lock); // 线程执行阻塞,此时自动执行解锁,当线程收到唤醒信号,函数立即返回,此时在进入临界区之前,再次自动加锁 printf("thread2 buf:%s\n", buf); // 临界区 sleep(1); pthread_mutex_unlock(&lock); } pthread_exit("thread2...exit"); } void *thread3_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); pthread_cond_wait(&cond,&lock); printf("thread3 buf:%s\n", buf); sleep(1); pthread_mutex_unlock(&lock); } pthread_exit("thread3...exit"); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2, thread3; if(pthread_cond_init(&cond,NULL) != 0){ errlog("pthread_cond_init error"); } if(pthread_mutex_init(&lock,NULL) != 0){ errlog("pthread_mutex_init error"); } if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } if (pthread_create(&thread3, NULL, thread3_handler, NULL) != 0) { errlog("pthread_create3 error"); } pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_join(thread3, NULL); pthread_mutex_destroy(&lock); pthread_cond_destroy(&cond); return 0; }点击复制复制失败已复制
编译并运行,结果如下:
$ gcc main.c && ./a.out hello thread2 buf:hello worl thread3 buf:worl test thread2 buf:test fff thread3 buf:fff点击复制复制失败已复制
线程1写入数据,线程2读取一次,再次写入,线程3读取一次,运行正常(线程2、线程3的读取顺序完全取决于当时情况)。
分析
由于线程2于线程3功能一致,因此只说明线程2即可。线程2的执行代码中,条件变量在执行睡眠时,必须执行先执行上锁,之后进行解锁。
线程在睡眠之前进行加锁操作,这一步是任何情况下都必须要做的。此时的互斥锁的作用是对 pthread_cond_wait()
函数的睡眠进行保护,保证在线程在睡眠的过程中是不会被打断的。一旦线程睡眠成功,那么此时 pthread_cond_wait()
函数除实现阻塞外,还将刚才持有的互斥锁解除,之所以出现这样的操作是为了避免死锁的产生。此时,互斥锁属于未锁定状态,其他线程也可以进行加锁,并执行睡眠操作,这样就不会影响其他线程执行睡眠。当线程被执行唤醒操作时, pthread_cond_wait()
函数立刻返回,会再次自动执行加锁,并进入之后的临界区,操作共享资源,此时互斥锁的功能为对临界区加锁,保证线程对共享资源操作的完整性。这样做的目的为了保证数据的正确,保证在任何一个时刻只有一个线程在访问共享资源。当执行完临界区之后,再进行最后的解锁处理。
因此,上述线程的睡眠操作涉及两次加锁、解锁处理,两次加锁、解锁的目的则完全不同,需要注意。而对线程1的唤醒处理,则并没有采用互斥锁操作,代码如下:
fgets(buf, N, stdin); buf[strlen(buf) - 1] = '\0'; pthread_cond_signal(&cond); 点击复制复制失败已复制
之所以并没有使用互斥锁去操作,是因为当前 fgets()
函数的功能是读取终端输入,当终端无输入时,函数本身是阻塞的,不会主动写入数据。因此,本身不需要引入互斥锁的操作。针对上述示例,下图展示了其互斥锁、条件变量结合的情况。
上面通过示例讲解了条件变量的操作,如果将线程1不设置为读取终端输入,使之变成一个非阻塞的情况,那么在执行唤醒操作的地方则需要进行互斥锁的操作,
为了证明线程每次都有写入,对线程设置 count
进行计数。可以看出线程1在唤醒操作前,对临界区进行锁处理,如果不加锁处理,那么一旦执行唤醒操作之后,其他线程开始读取共享资源,线程1此时循环再次进行写入,那么就会产生竞态。
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <string.h> #define N 32 #define errlog(errmsg) \ do { \ perror(errmsg); \ printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__); \ return -1; \ } while (0) char buf[N] = ""; pthread_cond_t cond; pthread_mutex_t lock; int count = 0; void *thread1_handler(void *arg) { while (1) { printf("count = %d\n", ++count); sleep(1); pthread_mutex_lock(&lock); strcpy(buf, "hello"); pthread_mutex_unlock(&lock); pthread_cond_signal(&cond); } pthread_exit("thread1...exit"); } void *thread2_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); pthread_cond_wait(&cond, &lock); printf("thread2 buf:%s\n", buf); sleep(1); pthread_mutex_unlock(&lock); } pthread_exit("thread2...exit"); } void *thread3_handler(void *arg) { while (1) { pthread_mutex_lock(&lock); pthread_cond_wait(&cond, &lock); printf("thread3 buf:%s\n", buf); sleep(1); pthread_mutex_unlock(&lock); } pthread_exit("thread3...exit"); } int main(int argc, const char *argv[]) { pthread_t thread1, thread2, thread3; if (pthread_cond_init(&cond, NULL) != 0) { errlog("pthread_cond_init error"); } if (pthread_mutex_init(&lock, NULL) != 0) { errlog("pthread_mutex_init error"); } if (pthread_create(&thread1, NULL, thread1_handler, NULL) != 0) { errlog("pthread_create1 error"); } if (pthread_create(&thread2, NULL, thread2_handler, NULL) != 0) { errlog("pthread_create2 error"); } if (pthread_create(&thread3, NULL, thread3_handler, NULL) != 0) { errlog("pthread_create3 error"); } pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_join(thread3, NULL); pthread_mutex_destroy(&lock); pthread_cond_destroy(&cond); return 0; }点击复制复制失败已复制
运行结果如下:
$ gcc main.c && ./a.out count = 1 count = 2 thread2 buf:hello count = 3 thread3 buf:hello count = 4 thread2 buf:hello count = 5 thread3 buf:hello count = 6 thread2 buf:hello count = 7 thread3 buf:hello count = 8 thread2 buf:hello count = 9 thread3 buf:hello count = 10 thread2 buf:hello点击复制复制失败已复制
从运行结果可以看出一个需要注意的点是:唤醒操作一定要发生在睡眠之后,否则没有任何效果。