标签: POSIX多线程 条件变量
前言
这两天看POSIX线程的时候,看到了一道题。问题大概这样的:启动三个线程,线程1打印A,线程2打印B,线程3打印C,三个线程打印的顺序要求为ABC,循环打印10次,也就是ABCABCABC….。这里的关键是要实现三个线程的协作,每次循环都要控制,只有线程1打印A,线程2再打印,然后再是线程3。这里涉及到了线程的同步问题,分别用到了互斥量和条件变量。
正文
线程同步
在多个控制线程共享内存的时候,需要确保每个线程看到的数据都是一致的。如果每个线程使用的变量,其他线程都不会修改,或者当变量是只读的时候,不会存在不一致的问题。但是当一个线程使用变量的时候,其他线程也可以读取或者修改的时候,就会出现数据不一致的问题。
在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与存储器写这两个周期交叉时,这种不一致就会出现。存储器访问周期是指连续启动两次独立的存储操作所需间隔的最小时间,存储器的两个基本的操作为读入和写出,指将数据在存储单元与存储寄存器(MDR)之间进行读和写。在读取变量的时候,只有要一个存储器周期,但是修改变量的时候需要两个存储器周期。以n++为例,这条语句并非原子操作,它可以分解为以下三个操作:
- 从内存单元读入寄存器
- 在寄存器中对变量++
- 将新的值写回内存单元
如果两线程几乎在同一时间对同一个变量做增量操作而不进行同步的话,结果可能出现比原来增加了1也可能增加了2,具体要看第二个线程的开始操作获取的数值。
互斥量
互斥量可以确保在同一时间内只有一个线程访问数据,本质上讲就是一把锁。对于使用了互斥量保护的数据而言,谁先得到这把锁谁就先访问到数据,并且把数据锁住不让其他线程访问,如果此时有其他线程想要访问数据会因没有得到锁而进行阻塞状态,在处理完数据之后,把锁解开,这时其他线程才可以访问。这时间其他线程看到锁解开了,就会由阻塞变为可运行状态。
这里由此也引出了,互斥量使用不当的情况下,容易产生死锁。例如,如果线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。还有程序中使用一个以上的互斥量时,如果一个线程一直占有第一个互斥量,并且试图锁住第二个互斥量时处于阻塞状态,但此时有一个线程在拥有了第二个互斥量时也在试图锁住第一个互斥量时,会因为两个线程都在相互请求另一个线程拥有的资源而无法前进,于是就产生死锁。
条件变量
条件变量是线程的另一种同步机制,与互斥一起使用时,允许线程以无竞争的等待方式等待特定的条件发生。它由互斥量保护,在线程改变条件之前必须首先锁住互斥量。
回到前面提到的问题。前面说了,解法用到了互斥量和条件变量,来看看怎么用的:
#include<iostream>
#include <pthread.h>
int n = 10;
int flag = 1;
pthread_cond_t printReady = PTHREAD_COND_INITIALIZER;
pthread_mutex_t printLock = PTHREAD_MUTEX_INITIALIZER;
void *printA(void *arg)
{
for (int i = 0; i < n; ++i) {
pthread_mutex_lock(&printLock);
while (flag != 1) {
pthread_cond_wait(&printReady, &printLock);
}
printf("%s", (char *)arg);
flag = 2;
pthread_cond_broadcast(&printReady);
pthread_mutex_unlock(&printLock);
}
return ((void *)1);
}
void *printB(void *arg)
{
for (int i = 0; i < n; ++i) {
pthread_mutex_lock(&printLock);
while (flag != 2) {
pthread_cond_wait(&printReady, &printLock);
}
printf("%s", (char *)arg);
flag = 3;
pthread_cond_broadcast(&printReady);
pthread_mutex_unlock(&printLock);
}
return ((void *)2);
}
void *printC(void *arg)
{
for (int i = 0; i < n; ++i) {
pthread_mutex_lock(&printLock);
while (flag != 3) {
pthread_cond_wait(&printReady, &printLock);
}
printf("%s", (char *)arg);
flag = 1;
pthread_cond_broadcast(&printReady);
pthread_mutex_unlock(&printLock);
}
return ((void *)3);
}
int main(void)
{
pthread_t tid1, tid2, tid3;
void *ret;
int err;
err = pthread_create(&tid1, NULL, printA, (void *)"A");
err = pthread_create(&tid2, NULL, printB, (void *)"B");
err = pthread_create(&tid3, NULL, printC, (void *)"C");
err = pthread_join(tid1, &ret);
err = pthread_join(tid2, &ret);
err = pthread_join(tid3, &ret);
return 0;
}
这里先来解释几个关于条件变量的函数:
pthread_cond_signal()
:发送一个信号给正在当前条件变量的线程队列中处于阻塞等待状态的线程,使其脱离阻塞状态,唤醒后继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。一般只给一个阻塞状态的线程发信号。假如有多个线程正在阻塞等待当前条件变量,则根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但pthread_cond_signal在多处理器上可能同时唤醒多个线程,当只能让一个被唤醒的线程处理某个任务时,其它被唤醒的线程就需要继续wait。POSIX规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,有些实现为了简便,在单处理器上也会唤醒多个线程。pthread_cond_wait()
:等待条件变量的特殊条件发生;pthread_cond_wait() 必须与一个pthread_mutex配套使用。该函数调用实际上依次做了3件事:对当前pthread_mutex解锁、把当前线程挂起到当前条件变量的线程队列、被其它线程的信号唤醒后对当前pthread_mutex申请加锁。如果线程收到一个信号被唤醒,将被配套的互斥锁重新锁住,pthread_cond_wait() 函数将不返回直到线程获得配套的互斥锁。需要注意的是,一个条件变量不应该与多个互斥锁配套使用。pthread_cond_broadcast()
:某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait。
在上面的代码中有一处比较重要,就是pthread_cond_wait()
调用包裹在一个while循环里面,我刚开始的时候是用if来判断的。以printA为例,当flag不满足时,会释放互斥量进入阻塞状态并等待条件发生。其他线程修改了flag之后,随即调用pthread_cond_broadcast()
唤醒其他正在等待条件变量的线程,这时其他两个线程都会从pthread_cond_wait()
调用处返回。但满足flag条件的线程只有一个,这时while循环会再次判断flag条件,满足条件的线程会被唤醒,不满足的会继续阻塞。如果将while改为if,那么那个不满足flag条件的线程也会被唤醒。这里while循环实际上的作用就是只将满足条件的线程唤醒,不满足的继续等待。while还有一个另外一个作用,即线程可能不是由pthread_cond_signal
和pthread_cond_broadcast
唤醒,而是被中断唤醒。这种情况下,也需要重新对flag进行判断。
–EOF–