multiple threads synchronization primitive: 多线程同步语义
多线程的同步语义是多线程编程的核心,线程之间通过同步语义进行通信,实现并发。C++ JAVA 中线程同步的基本原语是condition variable 和mutex构成的管程 ,OS操作系统课程中经常出现的信号量(Semaphore)语义在实际编程中比较少见。我目前工作中只用过mutex+condvar,或者在它们之上的高层抽象,C++11 中的future和promise.
那么C++11 中的标准库已经支持std::condition_variable and mutex 。 所谓线程同步,就是线程之间的通信 ,传统的线程之间通信利用的是shared memory 共享内存的方式。 比如说productor 和consumer model,生产者thread和消费者thread 如何相互通信,就是利用shared memory 的buffer,buffer是threads之间沟通的桥梁。 生产者消费者都可以write 和read buffer的data. 这就引入了race condition 竞争态,会造成各个thread 视角下的data invariance .单线程内我们read data的invariance被破坏。这跟指令重排或者编译器重排的问题不一样(内存么模型更强调的是happen before语义,两个线程视角下的数据不一致,而不是单个线程下的不一致),race condition 包括语句之间的race condition 和单个语句例如i++非原子性导致的race condition.根本原因都是threads之间穿插执行.
解决方法就是mutex,变并发为串行。同时mutex也可以用于两个线程视角下同个变量值线程不一致的问题. mutex有两层语义:
1. 保证了一个线程lock(mutex)和unlock(mutex)之间保护的语句 肯定在另外一个线程lock(mutex)之前可以visible。
2.原子操作,unlock 之前。别的线程不能执行。解决race condition
回到线程同步中,mutex保护的对象就是buffer这个共享内存, 我们用predictor谓词 表示判断的内容。
pthread_mutex_lock(&mutex); while (condition == FALSE) pthread_cond_wait(&cond, &mutex); pthread_mutex_unlock(&mutex); pthread_mutex_lock(&mutex); condition = TRUE; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex);
1. 为什么cond wait需要关联一个mutex互斥锁.因为我们需要mutex保护共享内存.一个线程调用wait之后,我们应该先将线程加入等待队列中,然后unlock mutex. 因为先加入等待队列,然后unlock的顺序,所以我们无法不传入mutex。
这要求我们生成的线程一定要先lock mutex,然后才能操作buffer。否则不存在约束。保证两个线程之间的同步。
以上的add the waiting queue 和unlock mutex 的原子性依赖一个前提条件:唤醒者在调用pthread_cond_broadcast或pthread_cond_signal唤醒等待者之前也必须对相同的mutex加锁。
如果没有这个条件,那么为了保证原子性我们需要在wait 和signal内部实现中引入mutexB 去实现真正的原子性依赖.
c++11 实现:
std::unique_lock<std::mutex> lk(m); cv.wait(lk, []{return processed;});
2.虚假唤醒用while 解决:
while(predictor)为了防止虚假唤醒。两方面原因:
- 第一个原因就是wait的系统调用system call 被信号中断了。这时候如果需要重试,那么在判断和重试之间有race condition,此时都是无锁状态的. 即便想加锁也来不及了。判断是否需要加锁和加锁的race condition。
- 另外的原因就是wait被唤醒之后,要lock互斥锁。假如在此之前有其他线程抢占了mutex锁,然后更新了predictor为false。这时候再次切换回来就会虚假唤醒
-
不能用if代替,一个生产者可能对应着多个消费者,生产者向队列中插入一条数据之后发出signal,然后各个消费者线程的pthread_cond_wait获取mutex后返回,当然,这里只有一个线程获取到了mutex,然后进行处理,其它线程会pending在这里,处理线程处理完毕之后释放mutex,刚才等待的线程中有一个获取mutex,如果这里用if,就会在当前队列为空的状态下继续往下处理,这显然是不合理的.
3. signal到底是放在unlock之前还是之后??
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
如果先unlock,再signal,如果这时候有一个消费者线程恰好获取mutex,然后进入条件判断,这里就会判断成功,从而跳过pthread_cond_wait,下面的signal就会不起作用;另外一种情况,一个优先级更低的不需要条件判断的线程正好也需要这个mutex,这时候就会转去执行这个优先级低的线程,就违背了设计的初衷。
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_cond_signal(&qready);
pthread_mutex_unlock(&qlock);
}
如果把signal放在unlock之前,消费者线程会被唤醒,获取mutex发现获取不到,就又去sleep了。浪费了资源.但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。
所以在Linux中推荐使用这种模式。
以上参考: