互斥量
线程的优势主要是能通过全局变量来共享信息,但在多线程程序中,必须要确保多个线程不会同时修改同一变量(临界资源),或者某一线程不会读取正由其他线程修改的变量。当程序以非原子方式访问共享资源有一定概率会发生错误。如下面程序:
//代码转自Linux_Unix系统编程手册
#include <pthread.h>
#include "tlpi_hdr.h"
static volatile int glob = 0; /* "volatile" prevents compiler optimizations
of arithmetic operations on 'glob' */
static void * /* Loop 'arg' times incrementing 'glob' */
threadFunc(void *arg)
{
int loops = *((int *) arg);
int loc, j;
for (j = 0; j < loops; j++) {
loc = glob;
loc++;
glob = loc;
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-loops") : 10000000;
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s != 0)
errExitEN(s, "pthread_create");
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s != 0)
errExitEN(s, "pthread_create");
s = pthread_join(t1, NULL);
if (s != 0)
errExitEN(s, "pthread_join");
s = pthread_join(t2, NULL);
if (s != 0)
errExitEN(s, "pthread_join");
printf("glob = %d\n", glob);
exit(EXIT_SUCCESS);
}
在上述代码中在对glob变量进行递增的时候,是通过三个步骤实现:1).定义一个局部变量loc保存glob. 2)将局部变量loc加1。 3)将局部变量重新赋值给glob。这三个操作并非是原子操作,这样可能导致如下执行策略(因操作系统的调度策略):
1. 线程1将glob值赋给局部变量loc.假设glob当前值为2000.
2. 线程1的时间片满,线程2上CPU执行
3. 线程2执行多次循环,将glob增至3000。此时线程2时间片满。
4. 线程1获得另一时间片,并从上次停止处恢复执行。线程1上次运行时,glob的值为2000,将其赋给loc, 然后递增loc,并将2001写给glob。此时,线程2之前递增的结果遭到了覆盖。
流程图如下:
为了避免线程在更新共享变量(glob)出现问题,必须使用互斥量来确保同时仅有一个线程可以访问某项共享资源,也就是说使用互斥量来保证对任意共享资源的原子访问。
互斥量的性质如下:
- 互斥量有两种状态:locked 和unlocked。任何时候至多有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或报错失败,具体取决于加锁时使用的方法、
- 一旦线程锁定互斥量,则成为该互斥量的所有者。只有所有者才能给互斥量解锁。
- 对互斥量加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。
- 如果释放互斥量有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态。第一个变为运行的线程就可以对互斥量加锁,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变成可用。
互斥量保护临界区资源如下:
加锁和解锁互斥量
加锁与解锁函数:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
在使用互斥量之前,必须首先对它进行初始化,可以把它设置为常量PTHREAD_MUTEX_INITIALIZER
, 或调用pthread_mutex_init
函数进行初始化。使用互斥量保护对全局变量的访问程序如下:
#include <pthread.h>
#include "tlpi_hdr.h"
static volatile int glob = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static void * /* Loop 'arg' times incrementing 'glob' */
threadFunc(void *arg)
{
int loops = *((int *) arg);
int loc, j, s;
for (j = 0; j < loops; j++) {
s = pthread_mutex_lock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_lock");
loc = glob;
loc++;
glob = loc;
s = pthread_mutex_unlock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_unlock");
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1) ? getInt(argv[1], GN_GT_0, "num-loops") : 10000000;
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s != 0)
errExitEN(s, "pthread_create");
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s != 0)
errExitEN(s, "pthread_create");
s = pthread_join(t1, NULL);
if (s != 0)
errExitEN(s, "pthread_join");
s = pthread_join(t2, NULL);
if (s != 0)
errExitEN(s, "pthread_join");
printf("glob = %d\n", glob);
exit(EXIT_SUCCESS);
}
互斥量死锁
当一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能出现死锁。如下图:
线程A和线程B分别锁住了互斥量1和2,接着试图对互斥量2和1进行加锁,这将导致线程A和B都相互阻塞等待,造成死锁。
避免死锁的两种方案:
-定义互斥量的层级关系。当多个线程对一组互斥量操作时,总是应该以相同顺序对该组互斥量进行锁定。
-“尝试一下,再恢复”。线程先使用函数pthread_mutex_lock()
锁定第一个互斥量,然后使用函数pthread_mutex_trylock()来锁定其余互斥量。如果任一
pthread_mutex_trylock()调用失败,那么该线程释放所有互斥量,经过一段时间再试。
条件变量
条件变量允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程等待于这一通知。
下面一个未使用条件变量的生产者与消费者模型有助于展示条件变量的重要性。
生产者线程:
static pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
static int avail=0;//待消费产品数量
s=pthread_mutex_lock(&mtx);
if(s!=0)
errExitEN(s,"pthread_mutex_lock");
avail++;
s= pthread_mutex_unlock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_unlock");
消费者线程:
for (;;) {
s = pthread_mutex_lock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_lock");
while (avail > 0) { /* Consume all available units */
/* Do something with produced unit */
numConsumed ++;
avail--;
printf("T=%ld: numConsumed=%d\n", (long) (time(NULL) - t),
numConsumed);
done = numConsumed >= totRequired;
}
s = pthread_mutex_unlock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_unlock");
上面代码主线程不停地循环检查变量avail的状态,造成CPU资源的浪费。如果采用条件变量,允许消费者线程休眠直至生产者线程的通知(产品生产完毕)去执行某些操作,则可以解决上述问题。
条件变量相关函数
条件变量的数据类型是pthread_cond_t
,类似于互斥量,使用条件变量前必须对其初始化。如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
通知与等待函数:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);//通知至少一个遭阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);//通知所有遭阻塞的线程
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);//等待
生产者-消费者例子中使用条件变量:
生产者线程代码:
s = pthread_mutex_lock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_lock");
avail++; /* Let consumer know another unit is available */
s = pthread_mutex_unlock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_unlock");
s = pthread_cond_signal(&cond); /* Wake sleeping consumer */
if (s != 0)
errExitEN(s, "pthread_cond_signal");
pthread_cond_wait()函数主要进行如下操作:
- 解锁互斥量mutex
- 阻塞调用线程,直至另一线程就条件变量cond发出信号
- 重新锁定mutex.
设计上步骤是因为通常情况下,消费者线程代码会以如下方式访问共享变量:
s = pthread_mutex_lock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_lock");
while(/*check that shared variable is not in state we want*/)
pthread_cond_wait(&cond,&mutex);
/*Now shared variable is in desired state;do some work*/
s = pthread_mutex_unlock(&mtx);
if (s != 0)
errExitEN(s, "pthread_mutex_unlock");
由以上代码可知,在对共享变量的两处访问都置于互斥量的保护之中,基本的步骤总结如下:
- 线程在准备检查共享变量的状态时锁定互斥量
- 检查共享变量的状态
- 如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量(原子操作)。
- 当线程因为条件变量的通知而被再度唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量。
总结
多线程应用程序必须使用互斥量和条件变量等同步原语来协调对共享变量的访问。互斥量提供了对共享变量的独占式访问。条件变量允许一个或多个线程等候通知:其他线程改变了共享变量的状态。
参考:
《Linux_Unix系统编程手册上》
《Unix系统高级编程》