1、 概述
互斥锁和条件变量总是可用来同步一个进程内的各个线程的。如果一个互斥锁或条件变量存放在多个进程间共享的某个内存区中,那么它还可以用于这些进程间的同步。
2、互斥锁:上锁与解锁
互斥锁指代相互排斥(mutual exclusion),它是最基本的同步形式。互斥锁用于保护临界区(critical region), 以保证任何时刻只有一个线程在执行其中的代码(假设互斥锁由多个线程共享),或者任何时刻只有一个进程在执行其中的代码(假设互斥锁由多个进程共享),保护一个临界区的代码的通常轮廓大体如下:
lock_the_mutex(...);
临界区
unlock_the_mutex(...);
既然任何时刻只有一个线程能够锁住一个给定的互斥锁,于是这样的代码保证任何时刻只有一个线程在执行其临界区中的指令。
下列三个函数给一个互斥锁上锁和解锁:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_trylock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
// 均返回:若成功则为0,若出错则为正的Exxr值
如果尝试给一个已由另外某个线程锁住的互斥锁上锁,那么pthread_mutex_lock将阻塞到该互斥锁解锁为止。pthread_mutex_trylock是对应的非阻塞函数,如果该互斥锁已锁住,它就返回一个EBUSY错误。
3、生产者-消费者问题
一个或多个生产者(线程或进程)创建着一个个的数据条目,然后这些条目由一个或多个消费者(线程或进程)处理。数据条目在生产者和消费者之间是使用某种类型的IPC传递的。
// prodcons2.c
/* include main */
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; /* read-only by producer and consumer */
struct
{
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput;
int nval;
} shared = { PTHREAD_MUTEX_INITIALIZER };
void *produce(void *), *consume(void *);
int main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons2 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads);
/* start all the producer threads */
for (i = 0; i < nthreads; i++)
{
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
/* wait for all the producer threads */
for (i = 0; i < nthreads; i++)
{
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
/* start, then wait for the consumer thread */
Pthread_create(&tid_consume, NULL, consume, NULL);
Pthread_join(tid_consume, NULL);
exit(0);
}
/* end main */
/* include producer */
void * produce(void *arg)
{
for ( ; ; )
{
Pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems)
{
Pthread_mutex_unlock(&shared.mutex);
return(NULL); /* array is full, we're done */
}
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
Pthread_mutex_unlock(&shared.mutex);
*((int *) arg) += 1;
}
}
void * consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
}
return(NULL);
}
/* end producer */
运行结果:
4、对比上锁与等待
现在展示互斥锁用于上锁(locking)而不能用于等待(waiting),我们把上一节中的生产者-消费者例子改为在所有生产者线程都启动后立即启动消费者线程。这样在生产者线程产生数据的同时,消费者线程就能处理它。但现在我们必须同步生产者和消费者,以确保消费者只处理已由生产者存放的数据条目。
// prodcons3.c
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; /* read-only by producer and consumer */
struct {
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput;
int nval;
} shared = { PTHREAD_MUTEX_INITIALIZER };
void *produce(void *), *consume(void *);
/* include main */
int main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons3 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
/* create all producers and one consumer */
Set_concurrency(nthreads + 1);
for (i = 0; i < nthreads; i++)
{
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
/* wait for all producers and the consumer */
for (i = 0; i < nthreads; i++)
{
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
Pthread_join(tid_consume, NULL);
exit(0);
}
/* end main */
void * produce(void *arg)
{
for ( ; ; )
{
Pthread_mutex_lock(&shared.mutex);
if (shared.nput >= nitems)
{
Pthread_mutex_unlock(&shared.mutex);
return(NULL); /* array is full, we're done */
}
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
Pthread_mutex_unlock(&shared.mutex);
*((int *) arg) += 1;
}
}
/* include consume */
void consume_wait(int i)
{
for ( ; ; )
{
Pthread_mutex_lock(&shared.mutex);
if (i < shared.nput)
{
Pthread_mutex_unlock(&shared.mutex);
return; /* an item is ready */
}
Pthread_mutex_unlock(&shared.mutex);
}
}
void * consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
consume_wait(i);
if (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
}
return(NULL);
}
5、条件变量:等待与信号发送
互斥锁用于上锁,条件变量则用于等待。这两种不同类型的同步都是需要的。条件变量是类型为pthread_cond_t的变量,以下两个函数使用了这些变量。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
// 均返回:若成功则为0,若出错则为正的Exx值
pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex 配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
/* include globals */
#include "unpipc.h"
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
/* globals shared by threads */
int nitems; /* read-only by producer and consumer */
int buff[MAXNITEMS];
struct {
pthread_mutex_t mutex;
int nput; /* next index to store */
int nval; /* next value to store */
} put = { PTHREAD_MUTEX_INITIALIZER };
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready; /* number ready for consumer */
} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
/* end globals */
void *produce(void *), *consume(void *);
/* include main */
int main(int argc, char **argv)
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc != 3)
err_quit("usage: prodcons6 <#items> <#threads>");
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
Set_concurrency(nthreads + 1);
/* create all producers and one consumer */
for (i = 0; i < nthreads; i++)
{
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);
}
Pthread_create(&tid_consume, NULL, consume, NULL);
/* wait for all producers and the consumer */
for (i = 0; i < nthreads; i++)
{
Pthread_join(tid_produce[i], NULL);
printf("count[%d] = %d\n", i, count[i]);
}
Pthread_join(tid_consume, NULL);
exit(0);
}
/* end main */
/* include prodcons */
void * produce(void *arg)
{
for ( ; ; )
{
Pthread_mutex_lock(&put.mutex);
if (put.nput >= nitems)
{
Pthread_mutex_unlock(&put.mutex);
return(NULL); /* array is full, we're done */
}
buff[put.nput] = put.nval;
put.nput++;
put.nval++;
Pthread_mutex_unlock(&put.mutex);
Pthread_mutex_lock(&nready.mutex);
if (nready.nready == 0)
Pthread_cond_signal(&nready.cond);
nready.nready++;
Pthread_mutex_unlock(&nready.mutex);
*((int *) arg) += 1;
}
}
void * consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
Pthread_mutex_lock(&nready.mutex);
while (nready.nready == 0)
Pthread_cond_wait(&nready.cond, &nready.mutex);
nready.nready--;
Pthread_mutex_unlock(&nready.mutex);
if (buff[i] != i)
printf("buff[%d] = %d\n", i, buff[i]);
}
return(NULL);
}
/* end prodcons */
6、条件变量:定时等待和广播
通常 pthread_cond_signal 只唤醒等待在相应条件变量上的一个线程。在某些情况下一个线程认定有多个其他线程应被唤醒,这时它可调用 pthread_cond_broadcast 唤醒阻塞在相应条件变量上的所有线程。
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cptr);
int pthread_cond_timedwait(pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
// 均返回:若成功则为0,若出错则为正的Exx值
pthread_cond_timedwait 允许线程就阻塞时间设置一个限制值。abstime参数是一个timespec结构:
struct timespec
{
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds*/
};
该结构指定这个函数必须返回时的系统时间,即便当时相应的条件变量还没有收到信号。如果发生这种超时情况,该函数就返回ETIMEDOUT错误。
时间值是绝对时间(absolute time),而不是时间差(time delta),这就是说, abstime是该函数应该返回时刻的系统时间——自UTC时间1970年1月1日子时以来流逝的秒数和纳秒数。这与select, pselect和poll (UNPVI第6章)不同,它们都指定在将来的某个小数秒数,到时函数应该返回(select指定将来的微秒数, pselect指定将来的纳秒数, poll指定将来的毫秒数)。使用绝对时间而不是时间差的好处是:如果函数过早返回了(也许是因为捕获了某个信号),那么同一函数无需改变其参数中timespec结构的内容就能再次被调用。
7、互斥锁和条件变量的属性
本章中的互斥锁和条件变量例子把它们作为一个进程中的全局变量存放,它们用于该进程内各线程间的同步。我们用两个常值PTHREAD_MUTEX_INITIALIZER和PTHREAD_COND_INITIALIZER来初始化它们。由这种方式初始化的互斥锁和条件变量具备默认属性,不过我们还能以非默认属性初始化它们。
首先,互斥锁和条件变量是用以下函数初始化或摧毁的。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mptr, const pthread_mutexattr_t *attr)
int pthread_mutex_destroy(pthread_mutex_t *mptr);
int pthread_condinit(pthread-cond_t *cptr, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cptr);
// 均返回:若成功则为0,若出错则为正的Ecx值
考虑互斥锁情况, mptr必须指向一个已分配的pthread_mutex_t变量,并由pthread_mutex-init函数初始化该互斥锁。由该函数第二个参数attr指向的pthread-mutexattr_t值指定其属性。如果该参数是个空指针,那就使用默认属性。
互斥锁属性的数据类型为pthread_mutexattr-t,条件变量属性的数据类型为pthread_condattr_t,它们由以下函数初始化或摧毁。
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
// 均返回:若成功则为0,若出错则为正的Ecx值
一旦某个互斥锁属性对象或某个条件变量属性对象已被初始化,就通过调用不同函数启用或禁止特定的属性。例如:指定互斥锁或条件变量在不同进程间共享,而不是只在单个进程内的不同线程间共享。这个属性是用以下函数取得或存入的。
#include <pthread. h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int value);
int pthread_condattr_getpshared(const pthread_condattr_t *attr, int *valptr);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int value);
// 均返回:若成功则为0,若出错则为正的Exx值
8、小结
互斥锁用于保护代码临界区,从而保证任何时刻只有一个线程在临界区内执行。有时候一个线程获得某个互斥锁后,发现自己需要等待某个条件变为真。如果是这样,该线程就可以等待在某个条件变量上。条件变量总是有一个互斥锁与之关联。把调用线程投入睡眠的pthread_cond_wait函数在这么做之前先给所关联的互斥锁解锁,以后某个时刻唤醒该线程前再给该互斥锁上锁。该条件变量由另外某个线程向它发送信号,而这个发送信号的线程既可以只唤醒一个线程(pthread_cond_signal),也可以唤醒等待相应条件变为真的所有线程(pthread_cond_broadcast)。
互斥锁和条件变量可以静态分配并静态初始化。它们也可以动态分配,那要求动态地初始化它们。动态初始化允许我们指定进程间共享属性,从而允许在不同进程间共享某个互斥锁或条件变量,其前提是该互斥锁或条件变量必须存放在由这些进程共享的内存区中。