为允许在线程或进程间共享数据,同步通常是必需的。互斥锁和条件变量是同步的基本组成部分。
1、互斥锁:上锁与解锁
此处小标题内容已在UNPv1第26章线程讲解中有所记录。
互斥锁指代相互排斥,是最基本的同步形式。互斥锁用于保护临界区,以保证任何时刻只有一个线程在执行其中的代码,或只有一个进程在执行其中的代码。
Posix互斥锁被声明为具有pthread_mutex_t数据类型的变量。有两种分配方式:
i、互斥锁变量是静态分配的,可以将其初始化为常值PTHREAD_MUTEX_INITIALIZER,如下所示,
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//采用静态分配初始化为常值
ii、互斥锁变量是动态分配的,或者分配在共享内存区,则必须在运行时通过调用pthread_mutex_init函数(具体内容见下面小标题)初始化。
三个函数给一互斥锁上锁和解锁:
#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_tyelock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
//均返回:若成功则为0,若出错则为正的Exxx值
如果尝试给一个已由另外某个线程锁住的互斥锁上锁,那么pthread_mutex_lock将阻塞到该互斥锁解锁为止。pthread_mutex_trylock是对应的非阻塞函数,如果该互斥锁已锁住,它就返回一个EBUSY错误。
互斥锁通常用于保护由多个线程或多个进程分享的共享数据。
2、生产者-消费者问题
生产者-消费者问题也称为有界缓冲区问题:一个或多个生产者(线程或进程)创建一个个的数据条目,然后这些条目由一个或多个消费者(线程或进程)处理。数据条目在生产者和消费者之间是使用某种类型的IPC传递的。
针对管道或消息队列而言,主要是由内核隐式处理同步问题(生产者超前消费者,供过于求,消费者超前生产者,供不应求)。
针对共享内存区用作生产者消费者之间的IPC形式时,生产者或消费者必须执行某种类型的显式同步,利用互斥锁+条件变量。
i、生产者先完成生产,消费者再获取条目使用
#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;//buff数组中下一次存放的元素下标
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);//告诉系统我们希望并发运行多少线程(只针对salaris系统有用,Unix下的线程本身是采用竞争形式)
/* 4start all the producer threads */
for (i = 0; i < nthreads; i++) {
count[i] = 0;
Pthread_create(&tid_produce[i], NULL, produce, &count[i]);//创建生产者线程
}
/* 4wait for all the producer threads */
for (i = 0; i < nthreads; i++) {
Pthread_join(tid_produce[i], NULL);//等待所有生产者线程终止,有点类似于进程中waipid函数的作用
printf("count[%d] = %d\n", i, count[i]);//输出每个线程的计数器值
}
/* 4start, then wait for the consumer thread */
Pthread_create(&tid_consume, NULL, consume, NULL);//创建单个消费者线程
Pthread_join(tid_consume, NULL);//等待消费者线程终止
exit(0);
}
/* 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;//相应生产者计数器+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 */
ii、生产者与消费者同时启动
这方式是生产者线程产生数据的同时,消费者线程就能处理它。具体代码如下:
#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);
/* 4create all producers and one consumer */
Set_concurrency(nthreads + 1);//只处+1是增加了一个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);
/* 4wait 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);
}
/* end consume */
自己对上述的程序consume_wait函数的理解(很通俗的话):当生产条目只到3000,而消费到3500时,此时消费者获取到cpu资源时,上锁,但由于供不应求,导致解锁释放cpu资源,同时进入下一次循环,由于互斥锁的存在,阻塞在等待获取cpu资源。不断轮询直至可以消费相应生产条目时才return。
3、条件变量:等待与信号发送
互斥锁用于上锁,条件变量则用等待,两者协同合作,为同步提供优质服务哈。
#include<pthread.h>
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
//此函数特点:将调用线程投入睡眠并释放调用线程持有的互斥锁,返回时,线程再次持有互斥锁
int pthread_cond_signal(pthread_cond_t *cptr);
//通过给予条件信号,唤醒正在睡眠中的线程
//均返回:若成功则为0,若出错则为正的Exxx值
利用互斥锁+条件变量改写生产者-消费者问题(避免了轮询而获取cpu资源):
/* 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);
/* 4create 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)//表时缓存池中可消费条目为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 */
以下给出条件变量发送信号代码的大体套路:
struct {
pthread_mutex_t mutex;
pthread_cond_t cond;
/*维护本件的各个变量*/
}var = {PTHREAD_MUTEX_INITIALIZER,PTHREAD_COND_INITIALIZER,...};
pthread_mutex_lock(&var.mutex);//上锁
/*设置条件为真*/
pthread_cond_signal(&var.cond);//给予条件信号
pthread_mutex_unlock(&var.mutex);//解锁
以下给测试条件并进入睡眠以等待该条件变为真的代码大体套路:
pthread_mutex_lock(&var.mutex);
while (/*条件为假*/)
pthread_cond_wait(&var.cond, &var.mutex);
/*修改条件*/
pthread_mutex_unlock(&var.mutex);
避免上锁冲突
Posix明确允许:调用pthread_cond_signal的线程不必是与之关联的互斥锁的当前属主。但如果需要可预见的调度行为,那么调用pthread_cond_signal的线程必须锁住该互斥锁。
4、互斥锁和条件变量的属性
两个常值PTHREAD_MUTEX_INITIALIZER和PTHREAD_COND_INITIALIZER来初始化互斥锁和条件变量,此方式具备默认属性,但还可以以非默认属性初始化它们。
i、互斥锁和条件变量用以下函数初始化或摧毁:
#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_cond_init(pthread_cond_t *cptr, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cptr);
//均返回:若成功则为0,若出错则为正的Exxx值
参数attr指向的pthread_mutex_t或pthread_condattr_t值指定其属性。如果该参数是个空指针,那就使用默认属性。
ii、互斥锁或条件变量属性由以下函数初始化或摧毁:
#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 *attr);
int pthread_ocndattr_destroy(pthread_condattr *attr);
//均返回:若成功则为0,若出错则为正的Exxx值
此处需要对互斥锁或条件变量的属性进行初始化或摧毁可理解成,初始化即获取动态内存,摧毁即释放动态内存。(事实也是这样)
iii、既然初始化了互斥锁或条件变量的属性,就要用函数结合相应参数去设置或获取相应属性。以下只介绍一个需要使用的属性值设置:指定互斥锁或条件变量在不同进程间共享,而不是只在单个进程内的不同线程间共享。
#include<pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *valptr);
int pthread_mutexattr_setpshared(const pthread_mutexattr_t *attr, int value);
int pthread_condattr_getshared(const pthread_condattr_t *attr, int *valptr);
int pthread_condattr_setshared(const pthread_condattr_t *attr, int value);
//均返回:若成功则为0,若出错则为正的Exxx值
上述函数中参数value可以是PTHREAD_PROCESS_PRIVATE或PTHREAD_PROCESS_SHARED,后者也称为进程间共享属性。
运用样例:
pthread_mutex_t *mptr;//pointer to the mutex in shared memory
pthread_mutexattr_t mattr;//mutex attribute datatype
mptr =/* some value that points to shared memory */;
pthread_mutexattr_init(&mattr);//锁属性初始化,即动态分配内存
#ifdef _POSIX_THREAD_PROCESS_SHARED
pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);//对互斥锁设置进程间共享属性
#else
# error this implementation does not support _POSIX_THREAD_PROCESS_SHARED
#endif
pthread_mutex_init(mptr, &mattr);//对锁进行初始化
此外还有一个问题:持有锁期间进程终止(参见书上P138)
以上知识点来均来自steven先生所著UNP卷二(version2),刚开始学习网络编程,如有不正确之处请大家多多指正。