什么是同步? 就是让线程之间按照一定的顺序去执行,例如线程A生产了一个物品,线程B才可以去执行消费。
为什么不能直接用锁去做同步?
例如设定锁X,锁Q
线程B要消费一个物品,则对生产队列做检查(先对队列加锁保护),即对X加锁,若X已被锁,则阻塞。
即 加锁Q 加锁X 消费 解锁Q
当线程A生产了一个物品时, 将锁X解锁, 线程B唤醒,开始消费。
即 加锁Q 生产 解锁X 解锁Q
但是这有一个问题,如果线程A连续生产了很多物品, 之后不再生产,那么B去消费时,是没有必要加锁的,直接从队列里去取即可。
所以B的逻辑应该为
加锁Q 检查队列是否为空,若为空,加锁X 消费 解锁Q
但这时还存在一个大问题: 当线程B卡在加锁X时,队列Q被上锁了, 那么后来想再生产时,就进不去了!
所以这时候我们引入了条件变量X。
当线程B发现队列为空时, 自动解锁Q,并等待条件变量X为“可行“。
如果当线程A生产了一个物品时,就给条件变量X赋值, 于是线程B唤醒,并立刻自动加锁Q,开始消费
#include
#include
struct msg{
struct msg *m_next;
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void process_msg(void){
struct msg *mp;
for(;;){
pthread_mutex_lock(&qlock);
//如果队列为空,则对qlock解锁,并等待队列中有消息插入
//否则直接取队头。
while(workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq; //从队头取出消息
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/*处理消息*/
}
}
void enqueue_msg(struct msg *mp){
//给队列加锁
pthread_mutex_lock(&qlock);
mp->m_next = workq; //插入一个消息到队头
workq = mp;
pthread_mutex_unlock(&qlock);
//告诉线程,qread准备完毕了,你可以读了。
pthread_cond_signal(&qread);
}
为什么要用while(workq != NULL) 而不是 if(workq != NULL) ?
因为线程唤醒和给队列Q加锁,可能不是同一时刻发生。
可能当你唤醒时,另一个线程又取走了消息, 于是当你给队列加锁后,可能队列依然为空。
故需要while循环, 当在while循环里发现队列不为空时, 队列已经被你上锁了。
二、屏障
如果要多个线程,同时等待一个条件,并且在等待结束后同时继续后面的工作
即例如: 一群人做完自己的工作后,必须等所有人的工作都完成了,主管才分配后的工作。
那么这种情况可以用屏障barrier。
书里用的例子是多线程排序。
把一个很大的数组划分为8块,
其中每块各自用堆排序进行排序。
当全部排完后,进行合并,用归并合并的方式,每次从8个数组头中选最小的取出,放入结果数组。
好处是在多核系统中,能够较好地利用线程效率。
#include
#include
#include
#include
#define NTHR 8
#define NUMNUM 8000000L
#define TNUM (NUMNUM/NTHR)
long nums[NUMNUM];
long snums[NUMNUM];
pthread_barrier_t b;
#ifdef SOLARIS
#define heapsort qsort
#else
extern int heapsort(void *, size_t,size_t,
int(*)(const void *, const void *));
#endif
//把对应地址区域的数据视作long类型,去比较大小。
int complong(const void *arg1, const void *arg2){
long l1 = *(long *)arg1;
long l2 = *(long *)arg2;
if(l1 == l2)
return 0;
else if(l1 < l2)
return -1;
else
return 1;
}
void thr_fn(void *arg){
long idx = (long)arg;
//以nums[idx]为起点,长度为TNUM的数据区域进行排序
heapsort(&nums[idx], TNUM, sizeof(long), complong);
//排序完后,等待大家一起返回。
pthread_barrier_wait(&b);
return ((void *)0);
}
void merge(){
long idx[NTHR];
long i, minidx, sidx, num;
for (i = 0;i < NTHR; i++)
idx[i] = i * TNUM; //偏移
//idx指那个小排序数组的当前指针
//数组中每被取走一个,idx加一。
for (sidx = 0;sidx < NUMNUM; sidx++){
num = LONG_MAX;
//从NTHR个队列的队头中,找到最小的那个,并取走
for(i = 0;i < NTHR; i++){
//注意越界限制
if( (idx[i] < (i+1)*TNUM) && (nums[idx[i]] < num)){
num = nums[idx[i]];
minidx = i;
}
}
//把选中的数字放入snums,即结果数组。
snums[sidx] = nums[idx[minidx]];
idx[minidx] ++;
}
}
int main(){
unsigned long i;
struct timeval start,end;
long long startusec, endusec;
double elapsed;
int err;
pthread_t tid;
srandom(1);
for( i = 0;i < NUMNUM;i++)
nums[i] = random();
gettimeofday(&start, NULL);
pthread_barrier_init(&b, NULL, NTHR+1); //NTHR+1是线程数量
for( i = 0; i < NTHR; i++){
//传入线程的参数是void类型的,所以要做类型转化
//传入的参数是每个线程要处理的数据的起始点。
err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM));
if(err != 0)
err_exit(err, "can't create thread");
}
pthread_barrier_wait(&b);
//统一处理完毕,到达屏障之后,再进行合并。
merge();
gettimeofday(&end, NULL);
startusec = start.tv_sec * 1000000 + start.tv_usec;
endusec = end.tv_sec * 1000000 + end.tv_usec;
elapsed = (double)(endusec - startusec) / 1000000.0;
printf("sort took %.4f seconds\n", elapsed);
for( i = 0;i < NUMNUM; i++)
printf("%ld\n", snums[i]);
exit(0);
}