1. 提出背景
如何实现父线程等待子线程的join功能?
基于自旋的方式可以实现,代码如下:
#include <pthread.h>
#include <cstdio>
volatile int done = 0;
void* child(void* arg) {
printf("child\n");
done = 1;
return NULL;
}
int main() {
printf("parent:begin\n");
pthread_t c;
pthread_create(&c, NULL, child, NULL);
while (done == 0)
;
printf("parent: end\n");
return 0;
}
分析:可以实现,不过通过自旋的方式,浪费CPU时间,非常低效
解决办法:条件变量
2. 条件变量定义
线程可以使用条件变量(condition variable)来等待一个条件变成真。条件变量是一个显式队列,当某些执行条件(即条件,condition)不满足时,线程可以把自己加入队列,等待该条件。另外某个线程,当它改变了上述状态时,就可以唤醒一个或者多个等待线程(通过在该条件上发信号),让它们继续执行。
核心观点:当前线程条件不满足则休眠等待,不占用CPU时间;满足条件则发送信号,唤醒等待线程。
3. Pthread基本语法
3.1 初始化信号量
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
3.2 休眠等待信号
pthread_cond_wait(pthread_cond_t* c, pthread_mutex_t* m);
参数都是指针,除了信号量,还有锁(Pthread中称为互斥量)
3.3 发送信号唤醒等待线程
pthread_cond_signal(pthread_cond_t* c);
参数为信号量指针
由此解决背景中的问题,基于信号量的解决方案
#include <cstdio>
#include <pthread.h>
int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
void thr_exit() {
pthread_mutex_lock(&m);
done = 1;
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
void* child(void* arg) {
printf("child\n");
thr_exit();
return NULL;
}
void thr_join() {
pthread_mutex_lock(&m);
while (done == 0)
pthread_cond_wait(&c, &m);
pthread_mutex_unlock(&m);
}
int main() {
printf("parent: begin\n");
pthread_t p;
pthread_create(&p, NULL, child, NULL);
thr_join();
printf("parent: end\n");
return 0;
}
分析:
1. 为什么要使用done?
答:done实现了不同线程之间的交流,睡眠,唤醒和锁都离不开它。举个没有done的反例,子线程先运行thr_exit中的pthread_cond_signal发送信号,父线程后运行,若没有done,直接调用pthread_cond_wait,则将永远醒不来。
2. 为什么要有锁?
答:为了避免竞态条件,举个反例,若父线程先运行thr_join,执行到done == 0成立,该要运行pthread_cond_wait被系统中断运行子线程,由于没有锁子线程可以修改done,修改为1,然后切换到父线程,由于不会再次判断条件,则父线程运行pthread_cond_wait,则父线程将永远不会被唤醒。
经验总结:发信号时总是持有锁
3. 生产者/消费者问题(有界缓冲器)
3.1 问题描述
假设有一个或多个生产者线程和一个或多个消费者线程。生产者把生成的数据项放入缓冲区;消费者从缓冲区取走数据项,以某种方式消费。
3.2 代码
#include <pthread.h>
#include <cstdio>
#define MAX 10
int buffer[MAX];
int fill_ptr = 0;
int use_ptr = 0;
int count = 0;
pthread_cond_t empty, fill;
pthread_mutex_t mutex;
void put(int value) {
buffer[fill_ptr] = value;
fill_ptr = (fill_ptr + 1) % MAX;
count++;
}
int get() {
int tmp = buffer[use_ptr];
use_ptr = (use_ptr + 1) % MAX;
count--;
return tmp;
}
void* producer(void* arg) {
size_t loops = (size_t) arg; //注意参数类型转换
for (int i = 0; i < loops; i++) {
pthread_mutex_lock(&mutex);
while (count == MAX)
pthread_cond_wait(&empty, &mutex);
put(i);
pthread_cond_signal(&fill);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
size_t loops = (size_t) arg;
for (int i = 0; i < loops; i++) {
pthread_mutex_lock(&mutex);
while (count == 0)
pthread_cond_wait(&fill, &mutex);
int tmp = get();
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
printf("%d\n", tmp);
}
return NULL;
}
分析
1. 为什么判断条件用while不用if ?
答:考虑有两个消费者线程,一个生成者线程的情况,如果一个消费者线程1由于资源不足等待信号,接着生产者线程生成资源发生信号,消费者线程进入就绪状态,但是这个时候中断发生,消费者线程2抢先执行并且消费了资源,然后再切换到消费者线程1,由于使用if只判断一次,不用再判断是否有资源,消费者执行消费操作,但是资源已经没有了,出现错误。
2. 为什么需要两个条件变量
答:如果只有一个条件变量,消费者线程除唤醒生产者线程外还可以唤醒其他消费者线程,同样的生产者线程除了唤醒消费者线程也可以唤醒其他生产者线程。举个反例,如果有一个生产者线程,两个消费者线程,首先生成者线程生成了一个资源,然后唤醒消费者线程1,消费了资源,接着消费者发送信号,此时有可能唤醒生成者线程(正确)也可能唤醒消费者线程2(错误),如果唤醒了消费者线程2,它发现没有资源了于是也进入休眠,那么此时三个线程都进入了休眠,将全部无法醒过来!!
3. 为什么需要设置loops?
答:这样可以使得生成者线程可以一次性生成多个资源,消费者线程可以一次性消费多个资源,提高了并发度
经验总结:条件变量的时候while循环判断
4. 覆盖条件
这里主要谈到如果无法判断要唤醒哪一个线程,那么可以唤醒所有的线程,使用pthread_cond_broadcast()可以实现,当然这种方式会影响性能,应该尽量避免,但是有时候也是一种选择(比如简单多线程内存分配库再内存资源不足后调用处理程序,现在有了新的内存空间,那么该唤醒哪一个之前无法获取内存的线程呢,作者是直接使用唤醒所有线程)。