目录
一、互斥和同步
1.1 互斥和同步的概念
1、互斥:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。这样的公共资源也成为临界资源。
2、同步:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 线程的运行依赖于 B 线程产生的数据。
二、同步方式
为了允许线程或进程之间共享数据,同步使必须的。同步多个线程或多个进程的方法有多种,主要有:互斥锁、条件变量、读写锁、信号量(包括Posix信号量、System信号量)。本文主要介绍互斥锁和条件变量,它们总是可用来同步一个进程内的多个线程的,如果一个互斥锁或条件变量存放在多个进程间共享的某个内存中(需要某种形式的共享内存),那么Posix还允许它用于这些进程之间的同步。
互斥锁和条件变量是同步的基本组成部分。
2.1互斥锁
2.1.1 介绍
互斥锁指代相互排斥(mutual exclusion),它是最基本的同步形式。互斥锁用于保护临界区(critical region),假设互斥锁由多个线程共享,通过使用互斥锁,可以保证任何时刻只有一个线程在执行临界区的代码。这对于进程也是一样的。为什么要保护临界区?因为在临界区中,往往有多个线程共享使用的公共资源,比如变量。
互斥锁本质上就是一把锁,线程或进程在访问临界区前对互斥锁进行设置(加锁),在访问后对互斥量进行释放(解锁)。互斥量加锁之后,任何其他试图再次对互斥量加锁的线程都会比阻塞,直到当前线程释放该互斥锁。保护一个临界区的伪代码可描述为:
lock_the_mutex(...);/*加锁*/
/*临界区*/
unlock_the_mutex(...);/*解锁*/
2.1.2 注意
【注1】:需要注意一点,访问某个共享资源的所有线程都要遵守某种互斥的数据访问规则,互斥机制才能正常工作。比如a,b,c,d,都会对共享资源source1进行写访问,只有a,b,c设计了相同的互斥数据访问规则,但是d没有,那么d没有锁的限制,就有可能和a,b,c中任一一个线程同时访问共享资源,造成互斥机制失效。因此,在编程时,要全面考虑线程对共享资源的使用情况,不要遗漏!
【注2】:在访问临界区前加锁,访问结束后立即解锁,临界区处于被锁状态的时间为锁的“粒度”。如果锁的“粒度”太粗,就会出现很多线程阻塞等待相同的锁,有损并发性能。如果锁的“粒度”太细,那么过多的锁开销会使系统性能受到影响,代码复杂。作为一个程序员,需要在满足锁的需求情况下,在代码复杂性和性能之间找到正确的平衡。
2.1.3 函数说明
posix互斥锁被声明为具有pthread_mutex_t数据类型的变量,通常被称为“互斥变量”或“互斥量”,为了简洁和统一,以下称pthread_mutex_t数据类型的变量为“互斥量”。
头文件:#include<pthread.h>
函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0;失败返回错误编号。
参数:
(1) mutex:pthread_mutex_t类型的互斥量。
(2) attr:互斥锁属性。此后有机会单独介绍,本文均设为NULL。
其它说明:
[说明1]:互斥量的初始化一般可分为两种,一种是静态分配方式,一种是动态分配方式。
a) 静态分配互斥量。定义全局变量,或加了static关键字修饰的局部变量。
/*可以直接使用常量PTHREAD_MUTEX_INITIALIZER进行初始化,或结构体中使用该常量进行初始化*/
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;/*全局变量 */
struct foo stru={1, PTHREAD_MUTEX_INITALIZER}; /*全局变量*/
/*也可以使用pthread_mutex_init()函数初始化,或结构体中定义互斥量并使用该函数对其初始化*/
pthread_mutex_t mtx; /*全局变量*/
pthread_mutex_init(&mtx);/*函数中初始化*/
struct foo stru={1, PTHREAD_MUTEX_INITALIZER}; ; /*全局变量*/
pthread_mutex_init(&(stru.f_lock), NULL);/*函数中初始化*/
b)动态分配互斥量。一般用于局部变量。使用malloc动态分配互斥变量,需要在内存释放前调用pthread_mutex_destory函数。
sturct foo *pfoo=malloc(sizeof(struct foo));/*alloc函数通过指针返回互斥量*/
pthread_mutex_init(&pfoo->mtx,NULL); /*alloc函数通过指针返回互斥量*/
...
pthread_mutex_destroy(&pfoo); /*release函数进行清理*/
free(pfoo);pfoo=NULL; /*release函数进行清理*/
综合来看:使用常量PTHREAD_MUTEX_INITIALIZER进行初始化较为方便简单,但是使用宏有两个需要注意的地方:第一不能初始化动态分配的互斥量,第二不能设置互斥量属性(“属性”以后有机会单独介绍,本文均设为NULL)。而函数pthread_mutex_init()比较灵活全面。
另,附上常量PTHREAD_MUTEX_INITIALIZER在/usr/include/pthread.h中定义:
/*/use/include/pthread.h*/
#ifdef __PTHREAD_MUTEX_HAVE_PREV
#define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
...
#else
#define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, { __PTHREAD_SPINS } } }
...
#endif
[说明2]:pthread_mutex_trylock函数功能:如果线程不希望等待已经上锁的资源而被阻塞,可通过此函数对互斥量“尝试”加锁。如果当前线程调用pthread_mutex_trylock时,互斥量未上锁(没有线程占用临界资源),那么当前线程将上锁成功,返回0,开始使用临界资源;如果当前线程调用pthread_mutex_trylock时,互斥量已经上锁,说明临界资源正在被占用,那么将立刻返回,errno设置错误EBUSY,并且不阻塞的继续向下运行线程。
2.1.4 程序:用于保护结构体的引用计数锁(互斥量)
在多线程工作环境中,可能多个线程需要访问某一个结构体对象。当多个线程需要访问某个动态分配的对象时,我们可以在对象中嵌入引用计数,确保在所有使用该对象的线程完成数据访问之前,该对象内存空间不会被释放。
引用计数:当前正在引用(使用)该对象的线程个数。
现在从头说起,比如,现在有一个结构体,现在用结构体动态创建一个对象,并用结构体对象指针struct foo *fp指向该对象。现在有三个线程:线程1,线程2,线程3,每个线程都要访问fp->sum,将自己的数据累加进去,最后获得三个线程对fp->sum累加结果。结构体现在如下:
typedef struct foo{
int sum;
} foo;
这里要考虑的是,如果仅仅是用简单的加减锁的方式,可能的访问顺序为:线程1加锁(线程2等待、线程3等待) ==>> 线程1访问fp->sum ==>> 线程1解锁 ==>> 线程2加锁(线程3等待) ==>>…。那,锁(信号量)的状态随着加锁和解锁,在1和0之间来回跳变,所以我们无法以锁是否为0,判断三个线程是否都已经访问过fp->sum,即,无法判断三个线程是否完成了对结构体对象fp成员sum的累加操作。
所以,要在结构体中加入“引用计数”count这一成员,记录线程的访问次数,有线程访问,就+1,访问完了就-1,这样,就可以通过fp->count是否为0,来判断三个线程是否访问完成。fp->count为0时,三个线程对fp->sum都访问完成,就可以销毁对象。可能的过程大概如下:线程1建立并访问fp->sum,fp->count加1 ==>>线程2建立并访问fp->sum,fp->count加1 ==>>线程3建立并访问fp->sum,fp->count加1 ==>> 线程1从join返回,fp->count减1 ==>> 。。。==>> 线程1从join返回,fp->count减1。加了引用计数count后的结构体定义如下:
typedef struct foo{
int count;
int sum;
} foo;
最后,我们就要考虑对三个线程都会访问的共享资源:fp->count、fp->sum,进行互斥访问的控制,最终来实现三个线程在进行“累加”这个并发任务上的同步。
为了避免多个线程对引用计数fp->count的同时访问,我们在结构体中,添加f_lock互斥量作为结构体成员,对fp->count进行互斥锁的控制。同样,添加sum_lock互斥量作为结构体成员,对fp->sum进行互斥锁的控制。添加两个互斥量后,结构体定义如下:
typedef struct foo{
int count;
pthread_mutex_t f_lock;
int sum;
pthread_mutex_t sum_lock;
} foo;
将程序思路梳理为如下流程图:
下面附上程序及运行结果:
/*************************************************************************
> File Name: main.c
> Author: hank
> Mail: 34392195@qq.com
> Created Time: 2020年07月23日 星期四 05时49分29秒
************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
/*线程个数*/
#define TOTAL_THREAD 10
/*多个线程共享的结构体--定义*/
typedef struct foo
{
int id;/*结构体对象的id*/
int count;/*引用计数*/
pthread_mutex_t f_lock; /*对<引用计数>操作时,进行互斥控制*/
int sum;/*多线程共享的临界资源:累加和*/
pthread_mutex_t sum_lock; /*对<累加和>操作时,进行互斥控制*/
/*more stuff here...*/
} foo;
struct foo *foo_alloc(int id)
{
struct foo *fp;
if((fp = malloc(sizeof(struct foo))) != NULL)
{
fp->id = id;
fp->count = 1;
fp->sum = 0;
if((pthread_mutex_init(&fp->f_lock, NULL) != 0) \
|| pthread_mutex_init(&fp->sum_lock, NULL) != 0)
{
free(fp);fp = NULL;
perror("struct foo mutex_init failed!\n");
return(NULL);
}
}
printf("struct foo<%d> malloc success!\n",fp->id);
return(fp);
}
/*创建线程时,foo对象多<引用计数>成员增加1*/
void foo_hold(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->count++;
printf("count in hold:%d\n", fp->count);
pthread_mutex_unlock(&fp->f_lock);
}
/*当调用foo对象的线程终止(join)时,foo对象的<引用计数>减少1*/
struct foo *foo_rele(struct foo *fp)
{
pthread_mutex_lock(&fp->f_lock);
fp->count--;
if(fp->count == 0) /*最后一个线程访问完成,释放结构体对象*/
{
pthread_mutex_unlock(&fp->f_lock);
/*释放两个动态分配的锁*/
pthread_mutex_destroy(&fp->f_lock);
pthread_mutex_destroy(&fp->sum_lock);
/*释放对象占用的堆空间,并重置指针*/
printf("count down to %d, free struct foo<%d> now!\n",fp->count,fp->id);
free(fp); //释放了fp指向的空间
fp=NULL;
}
else /*全部进程访问完毕*/
{
printf("count in rele:%d\n", fp->count);
pthread_mutex_unlock(&fp->f_lock);
}
return fp;
}
/*线程启动例程*/
void *thr_fn1(void *arg)
{
int a=10;
pthread_mutex_lock(&(((struct foo *)arg)->sum_lock));
((struct foo *)arg)->sum += a;
printf("sum = %d\n", ((struct foo *)arg)->sum);
pthread_mutex_unlock(&(((struct foo *)arg)->sum_lock));
return arg;
}
int main()
{
int thr_err;
struct foo *thr_ret;
/*创建线程数组*/
pthread_t tid[TOTAL_THREAD];
/*创建foo对象*/
struct foo *fp;
fp = foo_alloc(1001); /*创建对象*/
/*创建线程,每个线程成功进入线程启动例程后,对象的<引用计数>+1*/
int i;
for(i = 0; i < TOTAL_THREAD; i++)
{
if((thr_err = pthread_create(&tid[i], NULL, thr_fn1, (void *)fp)) != 0)
{
perror("pthread create failed!\n");
exit(1);
}
foo_hold(fp);
}
/*join线程,每个线程成功退出线程启动例程后,对象的<引用计数>-1*/
for(i = 0; i < TOTAL_THREAD; i++)
{
if((thr_err = pthread_join(tid[i], (void *)&thr_ret)) != 0)
{
perror("pthread join failed!\n");
exit(1);
}
fp = foo_rele(fp);
}
/*最后一次释放,将销毁对象*/
printf("****************************************\n");
fp = foo_rele(fp);
#if 0
printf("count=%d\n", fp->count);/*对象已经释放,访问不到才对*/
#endif
printf("****************************************\n");
exit(0);
}
运行截图:
2.1.5 死锁
1、死锁可能发生的情况:
(1) 如果线程试图对同一互斥量加锁两次,那么他自身就会陷入死锁状态。
(2) 程序中使用2个互斥量时,如果线程1一直占有互斥量X,线程2一直占有互斥量Y,线程1在占有互斥量X的同时,试图占有,并且已经阻塞等待互斥量Y,线程2在占有互斥量Y的同时,试图占有,并且已经阻塞等待互斥量X。这样,线程1和线程而2就形成了死锁状态。
使用1个以上的互斥量时,就可能出现这种情况。
2、解决方法:
(1) 可以要求程序员仔细控制互斥量加锁的顺序避免死锁的发生。假设需要对两个互斥量A和B同时加锁,如果所有线程总是在互斥量B加锁之前锁住互斥量A,那么这两个互斥量不会产生死锁。
但是,有时候应用程序的结构难以对互斥量严格按照顺序进行排序。比如,程序中涉及了太多锁和数据结构,可用的函数并不能把它转换成简单的层次,就很难应用此方法。
(2) 可以利用pthread_mutex_trylock接口避免死锁:如果已经占有某些锁,不能再获取锁A,那么可以先释放已经占有的锁,做好清理工作,过一段时间重新尝试获取锁A,直到获取到锁。获取到锁后,pthread_mutex_trylock接口返回成功,那么线程再向下执行。
(3) 使用pthread_mutex_timedlock函数(带有超时的锁)。如果已经占有某些锁,不能再获取锁A,那么等待一定的时间,如果时间超时还没有得到锁,那就设置errno为ETIMEDOUT。它和pthread_mutex_trylock立即返回相比,增加了可供选择的超时时间。
2.2 读写锁
2.2.1 简介
读写锁,又称“共享互斥锁”,和互斥锁类似,但相比互斥锁,读写锁允许更高的并行性。下面就类比以下互斥锁,总结一下读写锁的特点:
互斥锁有两种状态:加锁、不加锁;读写锁有三种状态:读模式下加锁,写模式下加锁,不加锁。
互斥锁一次只有一个线程可以对其加锁;读写锁一次只能有一个线程可以占有写模式的读写锁,但是可以有多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,无论是试图读还是试图写的线程都会被阻塞;当读写锁是读加锁状态时,试图写的线程会被阻塞直到所有线程都释放他们的读锁,试图读的线程不会被阻塞。
与互斥量相比,读写锁在使用之前必须初始化,在释放他们的底层内存之前必须销毁。
读写锁非常适合“对数据结构读的次数远大于写的次数”的情况。
2.2.2 函数
1、初始化和销毁函数
头文件:#include<pthread.h>
函数原型:
(1)int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
(2)int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
返回值:成功放回0,失败返回错误编号。
参数:
参数rwlock:在使用上,pthread_rwlock_t类似于pthread_t。但其实他们两者的定义有很大差别,在/usr/include/bits/pthreadtypes.h中可以查到两者的相关定义。typedef unsigned long int pthread_t,而pthread_rwlock_t是以一个结构体定义的。参数attr:读写锁属性。此后有机会单独介绍,本文均设为NULL。
其它说明:
[说明1]:attr在默认属性下,可通过PTHREAD_RWLOCK_INITIALIZER常量静态分配的读写锁进行初始化。
[说明2]:在释放内存前,需要调用destroy函数做清理工作!如果在destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。
2、加锁解锁函数
头文件:#include<pthread.h>
函数原型:
int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock);
返回值:成功放回0,失败返回错误编号。
其它说明:
[说明1]:如果要在读模式下锁定读写锁,调用rdlock函数;如果要在写模式下锁定读写锁,调用wdlock函数;不管哪种方式,unlock同一用于解锁。
[说明2]:系统的各种实现对共享模式(在读模式下锁定读写锁)下可获取的读写锁次数进行限制。所以需要检查rdlock的返回值。
[说明3]:tryrdlock和trywrlock是Single UNIX Specification定义的读写锁原语的条件版本。无法获取锁时,返回错误EBUSY。
3、带有超时的读写锁函数
头文件:
#include<pthread.h>
#include<time.h>
函数原型:
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);
返回值:成功放回0,失败返回错误编号。
其它说明:
[说明1]:可类比pthread_mutex_timedlock函数。时间超时如果还不能获得锁,那么函数将返回ETIMEDOUT错误。
2.3 自旋锁
2.3.1 简介
自旋锁和互斥量类似,但自旋锁不是通过休眠使线程阻塞的,而是通过忙等(循环自旋)使线程阻塞。
非自旋的锁会让线程进入休眠状态,当要重新获得锁时,线程要从休眠态切换至running态;而自旋是通过循环判断实现的,所以自旋锁会使线程一直处于running态。所以,自旋锁的优点是:线程不需要状态的转换,这样就减少了上下文切换,获取锁的速度快;缺点是:running态需要占用CPU,导致CPU资源的浪费。
综上,自旋锁适用于:锁被其它线程占用的时间较短(减少自旋锁占用CPU的时间),且当前线程并不希望在重新调度上花费太多的成本(尽快获取锁资源)。
事实上,有些unix操作系统的互斥量实现,并不是直接使线程变为休眠状态阻塞等待,而是先花一小段时间,进行一定次数的自旋尝试获取锁。达到某一阈值后,才会休眠。
自旋锁在非抢占式内核中是非常有用的,在这种内核中,自旋锁会阻塞中断,只有获取到自旋锁后,中断才会进入中断处理程序,所以同一时间段,只有一个CPU只处理一个中断处理程序,中断处理程序不会再被其它中断打破,中断处理程序就不会让系统陷入死锁状态。这种类型的内核中,中断处理程序不能休眠,因此他们能用的同步原语只能是自旋锁。
但是在用户层,自旋锁并不是很有用,甚至还可能发生死锁(见(2)-[说明1]),除非运行在不允许抢占的实时调度类中。
2.3.2 函数
1、初始化和销毁函数
头文件:#include<pthread.h>
函数原型:
(1)int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
(2)int pthread_spin_destroy(pthread_spinlock_t *lock);
返回值:成功返回0,失败返回错误编号。
参数:
pshared:进程共享属性,表示自旋锁是如何获取的。此后有机会单独介绍,本文均设为NULL。
-PTHREAD_PROCESS_SHARED:自旋锁能被可以访问锁底层内存的外部线程所获取。
-PTHREAD_PROCESS_PRIVATE:自旋锁只能被初始化该锁的进程内部的线程获取。
2、加锁解锁函数
头文件:#include<pthread.h>
函数原型:
(1)int pthread_spin_lock(pthread_spinlock_t *lock);
(2)int pthread_spin_trylock(pthread_spinlock_t *lock);
(3)int pthread_spin_unlock(pthread_spinlock_t *lock);
返回值:成功返回0,失败返回错误编号。
其他说明:
[说明1]:不要在持有自旋锁的线程中,调用可能会进入休眠状态的函数。因为如果持有自旋锁的线程进入休眠状态,其它自旋阻塞等待自旋锁的线程会持续占用CPU,浪费CPU资源。
这种情况在可抢占的内核中很可能发生:如果线程A获得自旋锁且还没有被释放,此时时间片到达要切换至其它线程,或者具有高调度优先级的线程抢占运行,就可能会使线程A进入休眠状态,阻塞在锁上的其它线程自旋的时间就会大于预期。
甚至此时获得CPU的线程可能会再试图获得自旋锁,导致死锁。
[说明2]:非抢占式内核能用的同步原语只能是自旋锁。(高编P335)
[说明3]:自旋锁在抢占式内核中要慎用!
2.4 条件变量
2.4.1 简介
条件变量是线程可用的一种同步机制。条件变量要与互斥量往往一起使用,互斥量用于上锁,条件变量用于阻塞等待与信号收发。
条件变量本身要由互斥锁保护,线程在改变条件状态之前必须首先锁住互斥量。
条件变量适合的情况是:假设线程A需要等待某个条件成立之后才能继续往下执行,如果当前这个条件不成立,那么该线程就在pthread_cond_wait上阻塞等待,直到另一个线程B在某个时刻使得这个条件成立,此时,线程B就通过函数pthread_cond_signal发送信号,唤醒线程A向下运行。
在这种类似生产者消费者的关系模型中,相比只使用互斥锁,使用互斥锁+条件变量的优点是:互斥锁在公用临界资源匮乏的时候,消费者线程就只能循环给互斥锁解锁又上锁,这样一直轮询测试(polling/spinning)资源是否可用,这种测试会极大浪费CPU的时间;而引入条件变量后,条件变量使用信号的机制,在公用临界资源匮乏时,让消费者阻塞起来,直到生产者补上资源后,发送信号,唤醒消费者线程,继续执行。这样,就通过信号机制,避免了无用的CPU等待。可参考https://www.cnblogs.com/xiaoshiwang/p/11041070.html
/*不使用条件变量和使用条件变量,消费者效率对比(此为部分代码,参考链接:https://www.cnblogs.com/xiaoshiwang/p/11041070.html)*/
//版本1:仅使用互斥量,不加入条件变量,轮询测试条件
void consume_wait(int i){ //轮询等待生产者加入资源
while(1){
pthread_mutex_lock(&shared.mutex); //加锁
if(i < shared.idx){
pthread_mutex_unlock(&shared.mutex);
return;
}
pthread_mutex_unlock(&shared.mutex); //释放锁,
}
}
void* consume(void* arg){
int i;
for(i = 0; i < nitem; ++i){
consume_wait(i);
if(shared.buf[i] != i){
printf("buf[%d] = %d\n", i, shared.buf[i]);
}
}
return NULL;
}
//版本2:加入条件变量
void* consume(void* arg){
int i;
for(i = 0; i < nitem; ++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(buf[i] != i){
printf("buf[%d] = %d\n", i, buf[i]);
}
}
printf("buf[%d] = %d\n", nitem-1, buf[nitem-1]);
}
书上说,“条件变量给多线程提供了一个会和的场所,条件变量和互斥锁一起使用时,允许线程以无竞争的方式等待特定的条件发生。”个人理解这里无竞争的方式等待意思是:在面对同一共享资源的两端,消费者在资源匮乏,等待生产者的资源时,会释放互斥锁,阻塞在条件变量上,这样避免了与生产者竞争互斥锁的情况。
2.4.2 函数
1、初始化和销毁
在使用条件变量之前要进行初始化,由pthread_cont_t数据类型表示的条件变量可以用两种方式进行初始化。
静态分配的条件变量一般用常量PTHREAD_COND_INITIALIZER进行初始化;可查看头文件/usr/include/pthread.h,了解常量PTHREAD_COND_INITIALIZER:
动态分配的条件变量必须用pthread_cond_init()函数进行初始化, 一般情况下,它和用常量PTHREAD_COND_INITIALIZER进行初始化的效果等价。但是,当需要控制条件变量属性时,必须使用pthread_cond_init()函数初始化。条件变量属性此后有机会单独介绍,本文均设为NULL。
头文件:#include<pthread.h>
函数原型:
(1) int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
(2) int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功返回0;失败返回错误编号。
参数:
-cond:动态分配的条件变量。
-attr:条件变量属性。此后有机会单独介绍,本文均设为NULL。
其他说明:
[说明1]:在条件变量底层的内存空间释放之前,应使用destroy函数对其进行反初始化。另外,反初始化的条件变量可以再次用init函数初始化。
2、条件变量等待、通知函数
头文件:#include<pthread.h>
函数原型:
(1)int pthread_cond_wait(pthread_cond_t *restrict cond, const pthread_mutex_t *restrict mutex);
(2)int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, ...
const struct timespec *restrict tsptr);
(3)int pthread_cond_signal(pthread_cond_t *cond);
(4)int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功返回0;失败返回错误编号。
参数:
-cond:动态分配的条件变量。
-mutex:与条件变量配合的互斥量。
-tsptr:超时时间。类似pthread_mutex_timedlock函数。指定我们愿意等待多长时间,通过timespec结构指定,且需要指定一个绝对数,即需要把“当前时间+等待时间”转换成timespec结构。
其他说明:
2.4.2 程序框架
前边说过,条件变量常与互斥量往往要一起使用,允许线程以无竞争的方式等待特定的条件发生。现在给出一种条件变量和互斥量结合的框架大致如下:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
void *thr_1(void *arg)
{
pthread_mutex_lock(&mtx);
while(/*条件A“被吃完”*/)
pthread_cond_wait(&cond, &mtx);
/*“吃掉”1个条件A;*/
/*完成满足条件A的作业*/
pthread_mutex_unlock(&mtx);
}
void *thr_2(void *arg)
{
pthread_mutex_lock(&mtx);
/*“喂食”条件A*/
pthread_mutex_unlock(&mtx);
pthread_cond_signal(&cond);
}
如上框架所示,<条件A“被吃完”>表达可以是计数为0,也可以是队列为空等资源耗竭的状态。当条件A没有“被吃完”时,将跳过条件变量的阻塞,直接<“吃掉”1个条件A>,进行线程的作业。当条件A“被吃完”时,调用线程进入条件变量pthread_cond_wait(&cond, &mtx)进行阻塞,并释放mutex锁,睡眠等待(这是一个原子操作)。
如果此时有多个线程阻塞在cond锁下,当有其它线程调用线程启动函数thr_2进行“喂食”后,将发送pthread_cond_signal(&cond),阻塞在cond锁的多个线程将会有一个线程得到cond锁(哪个线程获得锁是无序的,不确定的),进行while判断(这时肯定满足),向下运行。其它阻塞在cond上的线程,继续等待pthread_cond_signal(&cond)信号。
这里为什么要用while,而不是if,或者不加:假设有这样一个场景,此时条件A被吃完了,并且有一个消费者线程阻塞在条件变量上,在接下来的某个时刻,生产者<“喂食”条件A>,然后释放了互斥锁,此刻,在生产者发送cond信号给阻塞的消费者线程之前,又有一个消费者调用了thr_1,并立刻获得了锁,直接<"吃掉"1个条件A>。这时,如果生产者再发送cond信号唤醒阻塞的那个消费者线程,并且没有while重新判断条件,就会让这个消费者线程去吃一个“被吃完”的条件A。因此,必须要用while重新检查条件。