Linux进程间通信(IPC)_同步(1):互斥锁和条件变量(《Unix高级编程》《Unix网络编程-卷2》学习总结)

 

目录

 

一、互斥和同步

1.1  互斥和同步的概念

二、同步方式

2.1互斥锁

2.1.1 介绍

2.1.2 注意

2.1.3 函数说明

2.1.4 程序:用于保护结构体的引用计数锁(互斥量)

2.1.5 死锁

2.2 读写锁

2.2.1 简介       

2.2.2 函数

2.3 自旋锁

2.3.1 简介

2.3.2 函数

2.4 条件变量

2.4.1 简介

2.4.2 函数

2.4.2 程序框架


一、互斥和同步

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重新检查条件。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
UNIX网络编程.卷2:进程通信(第2版)》是一部UNIX网络编程的经典之作!进程通信(IPC)几乎是所有Unix程序性能的关键,理解IPC也是理解如何开发不同主机网络应用程序的必要条件。《UNIX网络编程.卷2:进程通信(第2版)》从对Posix IPC和System V IPC的内部结构开始讨论,全面深入地介绍了4种IPC形式:消息传递(管道、FIFO、消息队列)、同步(互斥条件变量、读写、文件与记录、信号量)、共享内存(匿名共享内存、具名共享内存)及远程过程调用(Solaris门、Sun RPC)。附录中给出了测量各种IPC形式性能的方法。   《UNIX网络编程.卷2:进程通信(第2版)》内容详尽且具权威性,几乎每章都提供精选的习题,并提供了部分习题的答案,是网络研究和开发人员理想的参考书。 W.Richard Stevens,国际知名的UNIX网络专家,备受赞誉的技术作家他1951年2月5日出生于赞比亚,后随父母回到美国中学时就读于弗吉尼亚菲什伯恩军事学校,1973年获得密歇根大学航空和航天工程学士学位,1975年至1982年,他在亚利桑那州图森市的基特峰国家天文台从事计算机编程工作,业余时喜爱飞行运动,做过兼职飞行教练这期他分别在1978年和1982年获得亚利桑那大学系统工程硕士和博士学位此后他去康涅狄格州纽黑文的健康系统国际公司任主管计算机服务的副总裁,1990年他回到图森,从事专业技术写作和咨询工作写下了多种经典的传世之作。 好不容易找到的高清版本,特意拿出来和大家共享。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值