LINUX下的条件变量与互斥锁的搭配使用

写这篇文章的起因很复杂。。
可以先叙述一下事情的经过:
首先,在看到APUE的第十二章12.6线程私有数据 时,对作者用线程私有数据实现的可重入的getenv函数有疑问,当时我的想法是,整个进程中只有一个线程在pthread_once(&init_done, thread_init)的调用中执行了thread_init函数,那么就只有一个pthread_key_t类型的key被创建。那么在每个线程调用setspecific(pthread_key_t key, void *addr)函数来设置私有数据空间的始址时,用的都是同一个key,那每个线程通过getspecific(pthread_key_t key)函数获得数据空间始址时,得到的也是相同的地址,那么还存在什么私有可言?
当时对APUE中的讲解看的不是很清楚。于是在CSDN上提问,后来有个好心的哥们儿告诉我说,原理是这样的:进程中的所有线程共用一个key,但每个线程通过这个key关联到的都只是自己的数据段,相互之间是不干扰的。就相当于key是一个map,线程从map中根据自己的线程ID来设置或是查找自己的私有数据段。


感谢CSDN上的大哥。
然后我手贱,想自己去验证一下,于是写了如下代码,开始了自己悲催的一天:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

static pthread_mutex_t mutex		= PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t	cond		= PTHREAD_COND_INITIALIZER;
static pthread_once_t	initflag	= PTHREAD_ONCE_INIT;
static pthread_key_t key;

static int nonzero = 0;	// set to nonzero by thread1_func

static void thread_init(void)
{
	printf("key init\n");
	pthread_key_create(&key, free);
}

static void* thread_func1(void *arg)
{
	int err = 0;
	char *p = NULL;
	pthread_once(&initflag, thread_init);
	// allocate memory for this thread associated to key
	p = pthread_getspecific(key);
	printf("thread1 first get: p = %p\n", p);
	if (NULL == p) {
		if ((p = (char*)malloc(8)) == NULL) {
			perror("malloc error");
			exit(EXIT_FAILURE);
		}
		if ((err = pthread_setspecific(key, p)) != 0) {
			fprintf(stderr, strerror(err));
			exit(EXIT_FAILURE);
		}
	}
	p = pthread_getspecific(key);
	printf("thread1 second get: p = %p\n", p);
	
	// tell thread_func2 to check its associated memory
	pthread_cond_signal(&cond);

	// wait thread_func1 end
	pthread_mutex_lock(&mutex);
	pthread_cond_wait(&cond, &mutex);
	pthread_mutex_unlock(&mutex);
	printf("thread1 return\n");
	return NULL;
}

static void* thread_func2(void *arg)
{
	char *p = NULL;
	pthread_once(&initflag, thread_init);
	// wait thread_func1 to allocate memory
	pthread_mutex_lock(&mutex);
	pthread_cond_wait(&cond, &mutex);
	pthread_mutex_unlock(&mutex);
	// check its own memory associated to key
	p = pthread_getspecific(key);
	printf("thread2 get: p = %p\n", p);
	// tell thread1 to end
	pthread_cond_signal(&cond);
	printf("thread2 return\n");
	return NULL;
}


int main()
{
	pthread_t tid1, tid2;
	printf("start:\n");
	if (pthread_create(&tid1, NULL, thread_func1, NULL) != 0) {
		perror("create thread1 error");
		exit(EXIT_FAILURE);
	}
	if (pthread_create(&tid2, NULL, thread_func2, NULL) != 0) {
		perror("create thread2 error");
		exit(EXIT_FAILURE);
	}
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}




当然这个代码不看也罢了。


我一开始想着这样就行了,后来怎么也编译不通过?!告诉我pthread_cond_signal没有定义!!无奈了!!
各种求助,但没人知道错误到底是什么。网上也找不到任何可参考的回答。
后来我真的没辙了,开始一行一行重写代码。写一句编译一次,我倒要看到底哪行出错!
后来重写了一遍,竟然没错!!! 我的妈呀,那原先的代码着魔了吗?
不甘心,然后一句一句把原先的代码往新的代码里换,还是,换一句编译一次。
最后换到pthread_cond_signal()那句时,终于被我发现了啊!!!!!
原来是我函数名写错了啊!!!我写成pthread_cond_sigal了啊!!!!
这谁能发现啊!!!!!!!!!!!!!!!!!


这时已经花了我两个多小时了。。。。
后来想着,好歹有了个正确的代码。开始执行。


俺先讲一下这个代码的过程:
主体思想就是,创建两个线程。第一个线程创建自己的私有数据空间,然后第二个线程检查自己的私有数据空间,然后这两个线程再退出。问题就出在这儿。上一句的两个“然后”是两个同步问题,即要等第一个线程创建了自己的地址空间,第二个线程再去检查。第二个同步是等第二个线程检查完毕,第一个线程再退出(这样是为了验证我原来的猜想是错的:当第二个线程检查到的数据地址为NULL时,可以确定原因是每个线程的私有数据互不干扰,而不是因为第一个线程提前退出造成的)。
为了实现这两个线程的同步关系,我采用了条件变量。当然,我说明了第一个同步的实现,第二个也就大同小异了。所以只说第一个同步:
第一个线程中的signal
void* thread_func1(void *arg)
{
pthread_once(&initflag, thread_init);
// ... 申请线程的私有数据空间
pthread_cond_signal(&cond);
// ...
}
第二个线程中的wait:
void* thread_func2(void *arg)
{
pthread_once(&initflag, thread_init);


pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
// ... 检查自己的私有数据空间
}


PS:当时我理解的条件变量就这么用,一个wait操作,一个signal操作。因为pthread_cond_wait时传入的第二个参数要求是被锁住的互斥锁,所以在wait的前后加了lock和unlock的操作。


但发现这个程序执行时,只是在“大多数情况下”是正确的。。。有的时候就会卡死,执行不下去了。
如图:第一张是正常执行时的输出:


这张是执行不下去的输出:






MD!!这到底是咋回事呀??


后来发现,很可能是第一个线程执行signal的时候,第二个线程太懒了,竟然还没开始wait!这怎么能行呢?
第一个想法是让线程1先sleep几秒钟,再开始执行。
可这样治标不治本,不可靠啊! 到底要怎么办呢?

后来才想出答案——问题的源头在这儿:条件变量到底是用来干嘛的?
记得最初实现同步的方法是整型信号量,即忙等方式,简单来说就是
while (!condition) {/* do nothing */}
// do whatever you want
但操作系统不希望有“忙等”出现,因为太耗费资源。于是,操作系统想干这个:
while (!condition) {
block();
}
直到condition成立的时候,再让线程从block中返回。这时,条件变量就出现了。这个条件变量的目的就是来打破这个线程阻塞的状态,即signal()。
但像我之前写的代码,在线程2中其实是没有了上面的while那句,让线程一开始就block(),然后就开始等呀等,等哪个条件变量来叫醒它。signal()函数就起到这么一个“叫醒”的作用。
但如果线程2执行晚了,也就是说,别人之前来叫过它,但它不在,后来它再睡着的时候,就没有人再来叫它了。所以就出现了我之前执行不下去的情况。


那什么是正确用法呢?应该是这样:别人来叫它,如果发现它不在的话,就留张纸条儿。等那个线程来的时候它一看纸条,啊哈!已经有人叫过我了,我就不能再睡了,再睡就起不来了!于是这个线程就高高兴兴地执行下面的代码,而不去阻塞了。
所以——正确的做法是,设置一个变量。比方说定义一个全局的整型变量 nonzero,初值为0;每次线程2都会检查,如果nonzero是0的话,就去睡觉,如果不是0的话,就执行睡完觉的代码。而线程1则是应该先将nonzero置1,再调用signal,试图把线程2叫醒——如果线程2在睡觉,那它就被叫醒了。如果线程2还没开始睡,那它一会儿过来的时候会看到我给它留的纸条儿(nonzero==1),它也自然不会去睡了。


至此我才明白,为什么signal()前后要用锁保护,是因为signal()之前要更改一个条件(即写一张纸条儿),而这个纸条儿是多个线程同时访问的(即可能有多个人写,但也可能有人把它擦了),所以要在纸条上加互斥锁。这时应该不难理解,写纸条和叫醒另一个线程最好要成为原子操作才可靠。
所以正确的唤醒步骤应该是:
1、对互斥量加锁;
2、改变互斥量保护的条件;
3、发送信号;
4、对互斥量解锁。

当然也有一些讨论是说第三步和第四步是否可以交换的问题。如果维持原样,在一些操作系统上可能会有性能的损失(将wait线程唤醒,wait线程又无法继续加锁,又开始阻塞)。如果交换3、4步,即先解锁再发送消息,那么这两步之间可能会有别的线程再次占有锁,进而改变互斥量保护的条件。这一点由于wait是嵌套在while(!condition)循环中的,所以这时在逻辑上不会出错。但实质上是wait错过了一次被唤醒的机会。这个还需依使用环境而定。在LINUX下推荐不交换,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。




而为什么wait()前后也要加互斥锁保护,是因为wait到了之后,它要把这个纸条的内容擦掉。设想下面这种情况:当线程2从wait()返回后,如果有线程3此时刚要执行while (!condition),那么此时线程3判断这个条件是符合的,于是线程3也不wait()了,就高高兴兴跳过while执行后面的代码了。于是,一个signal就叫醒了两个线程(当然线程3不是被叫醒的,不过一个意思哈)。
所以,当线程从wait()返回后,应该要恢复被互斥锁保护的条件。而恢复的过程当然要用互斥锁保护了。不难理解,wait()和恢复这两个操作也应合成一个原子操作。
所以正确的wait()操作是:
1、对互斥量加锁;
2、在while (!condition)中wait();
3、恢复互斥量保护的条件;
4、对互斥量解锁


这里备注一点:我对wait前后加互斥锁保护的理由是,在应用中,往往在wait到之后,要恢复互斥量保护的条件。然而,这只是应用中的事情,为什么POSIX规定,wait函数前后一定要用加锁/解锁包围呢?

一个常见的解释是,执行wait时,要对互斥量解锁,再在条件变量上阻塞。所以,如果wait前后的没有互斥量的话,可能在解锁之后,阻塞之前,别的线程发出了signal,而本线程因为没有被阻塞,所以无法捕获到signal信号。

我觉得这个理由有点扯,如果wait直接就不需要互斥量的话,在执行wait的时候,就不需要对互斥量进行解锁。也就是只有阻塞的一步了。

更好的理由是:wait的互斥量用于保证在条件变量的阻塞过程的原子性。也就是说,在wait已经调用,但还未阻塞的时候,无法捕捉到其他线程的signal。所以要保证阻塞过程的原子性。参见pthread_cond_signal虚假唤醒中的《消息遗漏》部分。然而,这就必须将signal语句也包含在加锁/解锁的范围中,才能保证没有消息遗漏。也就是,上述唤醒步骤的3、4步不可交换。


好吧,最后还是觉得,这是一个通用的用法,就像我最开始理解的意义一样,因为wait和signal往往伴随着真正的条件的改变,而真正的条件是共享变量,所以要用锁来保护吧。附一个知乎链接:pthread_cond_wait 为什么需要传递 mutex 参数?


另外,为什么pthread_cond_wait()要用while语句包围而不用if判断,之前我以为有了锁变量,就不需要while,但后来发现,由于以下两条理由,故一定要用while包围。

1. 在多处理器的时候,有虚假唤醒的情况。所以必须要用while来保护,来检测是否为虚假唤醒(具体机制还不是太了解)。参见pthread_cond_signal虚假唤醒(spurious wakeup)

2. signal之后,wait被唤醒的过程中,有一个对mutex重新加锁的过程。那么,就可能导致该wait没有获得到锁,而被另一个线程获得,而后对while中的条件做了更改。如果用if的话,就不会再次检测条件的合法性。


附上最后的代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

static pthread_mutex_t mutex		= PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t	cond		= PTHREAD_COND_INITIALIZER;
static pthread_once_t	initflag	= PTHREAD_ONCE_INIT;
static pthread_key_t key;

static int nonzero = 0;	// set to nonzero by thread1_func

static void thread_init(void)
{
	printf("key init\n");
	pthread_key_create(&key, free);
}

static void* thread_func1(void *arg)
{
	int err = 0;
	char *p = NULL;
	pthread_once(&initflag, thread_init);
	// allocate memory for this thread associated to key
	p = pthread_getspecific(key);
	printf("thread1 first get: p = %p\n", p);
	if (NULL == p) {
		if ((p = (char*)malloc(8)) == NULL) {
			perror("malloc error");
			exit(EXIT_FAILURE);
		}
		if ((err = pthread_setspecific(key, p)) != 0) {
			fprintf(stderr, strerror(err));
			exit(EXIT_FAILURE);
		}
	}
	p = pthread_getspecific(key);
	printf("thread1 second get: p = %p\n", p);
	
	// tell thread_func2 to check its associated memory
	pthread_mutex_lock(&mutex);
	nonzero = 1;
	pthread_cond_signal(&cond);
	pthread_mutex_unlock(&mutex);

	// wait thread_func1 end
	pthread_mutex_lock(&mutex);
	while (0 == nonzero) {
		pthread_cond_wait(&cond, &mutex);
	}
	nonzero = 1;
	pthread_mutex_unlock(&mutex);
	printf("thread1 return\n");
	return NULL;
}

static void* thread_func2(void *arg)
{
	char *p = NULL;
	pthread_once(&initflag, thread_init);
	// wait thread_func1 to allocate memory
	pthread_mutex_lock(&mutex);
	while (0 == nonzero) {
		pthread_cond_wait(&cond, &mutex);
	}
	nonzero = 0;
	pthread_mutex_unlock(&mutex);
	// check its own memory associated to key
	p = pthread_getspecific(key);
	printf("thread2 get: p = %p\n", p);
	// tell thread1 to end
	pthread_mutex_lock(&mutex);
	nonzero = 1;
	pthread_cond_signal(&cond);
	pthread_mutex_unlock(&mutex);
	printf("thread2 return\n");
	return NULL;
}


int main()
{
	pthread_t tid1, tid2;
	printf("start:\n");
	if (pthread_create(&tid1, NULL, thread_func1, NULL) != 0) {
		perror("create thread1 error");
		exit(EXIT_FAILURE);
	}
	if (pthread_create(&tid2, NULL, thread_func2, NULL) != 0) {
		perror("create thread2 error");
		exit(EXIT_FAILURE);
	}
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	return 0;
}


至此才明白,为什么APUE的作者要这么用。原来作者博大精深,我等还差的太多啊!!


注:因为附的源码比截图时源码的略有改动,所以输出信息稍有出入,但不影响理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值