Linux多线程临界区问题

临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待,有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。只能被单一线程访问的设备,例如:打印机。
看下面一个例子,
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM_THREAD 100
void *thread_inc(void *arg);
void *thread_des(void *arg);
long long num=0;

int main()
{
	pthread_t id[NUM_THREAD];
	int i;
	printf("sizeof long long:%d \n",sizeof(long long));
	for(i=0;i<NUM_THREAD;i++){
		if(i%2)
		   pthread_create(&(id[i]),NULL,thread_inc,NULL);
		else
	 	   pthread_create(&(id[i]),NULL,thread_des,NULL);
	}
	for(i=0;i<NUM_THREAD;i++)
		pthread_join(id[i],NULL);
	printf("result:%lld \n",num);
	return 0;
}

void *thread_inc(void *arg)
{
	int i;
	for(i=0;i<50000000;i++)
		num+=1;
	return NULL;
}

void *thread_des(void *arg)
{
	int i;
	for(i=0;i<50000000;i++)
		num-=1;
	return NULL;
}

运行之后发现,虽然每次运行的结果不同,但运行结果并不是0。存在的问题正是2个线程正在同时运行访问全局变量num。任何内存空间----只要被同时访问----都可能发生问题。

下面是存在的问题:

1、2个线程准备将变量num(num=99)的值加1的情况,在此状态下,线程1将变量num值增加到100后,线程2再访问num时,变量num中将按照预想的保存101。值的增加需要CPU运算完成,变量num中的值不会自动增加。线程1首先读取该变量的值并将其传递到CPU,获得加1后的结果100,最后再把结构写回变量num,这样num中就保存100。

2、接下来是线程2的执行过程:变量num中将保存101,这是理想情况。但是线程1完全增加num值之前,线程2完全有可能通过切换得到CPU资源,存在这样一种情况:线程1读取变量num的值并完成加1运算时,加1后的结果尚未写入变量num。现在执行流程跳转到了线程2,此时线程2 完成了加1运算,并将加1之后的结果写入变量num。因为变量num的值尚未被线程1加到100,因此线程2读到的变量num的值为99,结果是线程2将num值改成100。因此还剩下线程1将运算后的值写入变量num的操作。

3、此时线程1将自己的运算结果100在此写入变量num,结果变量num变成100.虽然线程1和线程2各做了1次加1运算,却得到了意想不到的结果。因此,线程访问变量num时应该阻止其他线程访问,直到线程1完成运算,这就是同步。


临界区位置

临界区定义:函数内同时运行多个线程时引起问题的多条语句构成的代码块。全局变量num不是临界区,因为它不是引起问题的语句。临界区通常位于由线程运行的函数内部。
下面是上述例程的两个函数
void *thread_inc(void *arg)
{
   int i;
   for(i=0;i<5000000;i++)
      num+=1;  //临界区
   return NULL;
}

void *thread_des(void *arg)
{
   int i;
   for(i=0;i<5000000;i++)
      num-=1;  //临界区
   return NULL;
}
临界区并非num本身,而是访问num的2条语句。这两条语句可能由多个线程同时运行,也是引起问题的直接原因。产生的问题可以整理为如下3种情况:
--2个线程同时执行 thread_inc函数
--2个线程同时执行 thread_des函数
--2个线程同时分别执行 thread_inc函数和 thread_des函数

线程同步
线程同步用于解决线程访问顺序引发的问题,需要同步的情况可以从如下两方面考虑
1、同时访问同一内存空间时发生的情况
2、需要指定访问同一内存空间的线程执行顺序的情况
控制线程执行顺序是指:假设有A、B两个线程,线程A负责向指定内存空间写入数据,线程B负责取走该数据。这种情况下,线程A首先应该访问约定的内存空间并保存数据。万一线程B先访问并取走数据,将导致错误结果。像这种需要控制执行顺序的情况也需要使用同步技术。

互斥量
互斥量即Mutual Exclusion,表示不允许多个线程同时访问。互斥量主要用于解决线程同步访问的问题。线程中为了保护理解区需要引入锁机制,互斥量就是一把锁,下面是互斥量的创建及销毁函数
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//成功时返回0,失败时返回其他值
mutex---创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值
attr---传递时将创建的互斥量属性,没有特别需要指定的属性时传递NULL
从上述函数声明中也可以看出,为了创建相当于锁系统的互斥量,需要声明如下pthread_mutex_t mutex;
该变量的地址将传递给pthead_mutex_init函数,用来保存操作系统创建的互斥量。调用pthread_mutex_destroy函数时同样需要该信息。
下面是利用互斥量锁住或释放临界区时使用的函数
#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//成功时返回0,失败时返回其他值
进入临界区前调用的函数就是 pthread_mutex_lock。调用该函数时,发现有其他进程已进入临界区,则 pthread_mutex_lock函数不会返回,直到里面的线程调用 pthread_mutex_unlcok函数退出临界区为止。也就是说,其他线程让出临界区之前,当前线程将一直处于阻塞状态。下面是保护临界区的代码块编写方法
pthread_mutex_lock(&mutex);
//临界区的开始
//......
//临界区的结束
pthread_mutex_unlock(&mutex);
需要注意的是,线程退出临界区时,如果忘了调用 pthread_mutex_unlock函数,那么其他为了进入临界区而调用 pthread_mutex_lock函数的线程就无法摆脱阻塞状态。这种情况称为“ 死锁”,下面利用互斥量解决示例中遇到的问题
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#define NUM_THREAD 100
void* thread_inc(void *arg);
void* thread_dec(void *arg);

long long num=0;
pthread_mutex_t mutex; //保存互斥量读取值的变量

int main()
{
	pthread_t thread_id[NUM_THREAD];
	int i;
	pthread_mutex_init(&mutex,NULL);
	for(i=0;i<NUM_THREAD;i++){
		if(i%2)
		  pthread_create(&(thread_id[i]),NULL,thread_inc,NULL);
		else
		  pthread_create(&(thread_id[i]),NULL,thread_dec,NULL);
	}
	for(i=0;i<NUM_THREAD;i++)
		pthread_join(thread_id[i],NULL);
	printf("result:%11d \n",num);
	pthread_mutex_destroy(&mutex);  //销毁互斥量
	return 0;
}

void *thread_inc(void *arg)
{
	int i;
	pthread_mutex_lock(&mutex);
	for(i=0;i<50000;i++)
		num+=1; //临界区
	pthread_mutex_unlock(&mutex);
	return NULL;
}
void *thread_dec(void *arg)
{
	int i;
	for(i=0;i<50000;i++)
	{
	pthread_mutex_lock(&mutex);
	num-=1;    //临界区
	pthread_mutex_unlock(&mutex);
	}
	return NULL;
}
运行结果为0,已经解决了上述示例的问题。但确认运行结果需要等待较长的时间。因为互斥量 lock、unlock函数的调用过程比想象中花费更长的时间。下面是 thread_inc函数的同步过程
void *thread_inc(void *arg)
{
   int i;
   pthread_mutex_lock(&mutex);
   for(i=0;i<50000000;i++)
      num+=1;
   pthread_mutex_unlock(&mutex);
   return NULL;
}
以上临界区划分范围较大,但这是考虑到---最大限度减少互斥量 lock、unlock函数的调用次数。

thread_des函数比thread_inc函数多调用49999999次互斥量lock、unlock函数。如果不太关注线程的等待时间,可以适当扩展临界区。但变量num的值增加到50000000前不允许其他线程访问,反而成了缺点。

信号量
下面所介绍的信号量是“二进制信号量”(只用0和1)完成控制线程顺序为中心的同步方法。下面给出信号量创建及销毁方法。

#include<semaphore.h>
int sem_init(sem_t *sem,int pshared,unsigned int value);
int sem_destroy(sem_t *sem);
//成功时返回0,失败时返回其他值
sem---创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值
pshared---传递其他值时,创建由多个进程共享的信号量;传递0时,创建只允许1个进程内部使用的信号量。
value---指定新创建的信号量初始值

#include<semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
//成功时返回0,失败时返回其他值
sem---传递保存信号量读取值的变量地址值,传递给 sem_post时信号量增1,传递给 sem_wait时信号量减1
调用 sem_init函数时,操作系统将创建信号量对象,此对象中记录着“信号量值”整数。该值在调用 sem_post函数时增1,调用 sem_wait函数时减1。但信号量的值不能小于0,因此在信号量为0的情况下调用 sem_wait函数时,调用函数的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post函数,信号量的值将变为1,而原本阻塞的线程可以通过将该信号量重新减为0并跳出阻塞状态。实际上通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为1)。
sem_wait(&sem); //信号量变为0...
//临界区的开始
//......
//临界区的结束
sem_post(&sem); //信号量变为1...
调用 sem_wait函数进入临界区的线程在调用 sem_post函数前不允许其他线程进入临界区。信号量的值在0和1之间跳转,因此,具有这种机制称为二进制信号量。下面给出信号量相关示例,此是关于控制访问顺序的同步,即与如下场景类似:
线程A从用户输入得到值后存入全局变量num,此时线程B将取走该值并累加。该过程共进行5次,完成后输出总和并退出程序。
按照线程A、线程B的顺序访问变量num,且需要线程同步,下面是示例:
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>

void *read(void *arg);
void *accu(void *arg);

static sem_t sem1;  //生成的信号量1
static sem_t sem2;  //生成的信号量2
static int num;

int main()
{
	pthread_t id1,id2;
	sem_init(&sem1,0,0);
	sem_init(&sem2,0,1);
	pthread_create(&id1,NULL,read,NULL);
	pthread_create(&id2,NULL,accu,NULL);
	pthread_join(id1,NULL);
	pthread_join(id2,NULL);
	sem_destroy(&sem1);
	sem_destroy(&sem2);
	return 0;
}

void *read(void *arg)
{
	int i;
	for(i=0;i<5;i++)
	{
	     fputs("Input num:",stdout);
	     sem_wait(&sem2);//信号量2为了防止在调用accu函数的线程还未取走数据的情况下,调用read函数的线程覆盖原值
	     scanf("%d",&num);
	     sem_post(&sem1);//信号量1为了防止调用read函数的线程写入新值前,accu函数取走数据
	}
	return NULL;
}
void *accu(void *arg)
{
	int sum=0,i;
	for(i=0;i<5;i++)
	{
		sem_wait(&sem1);
		sum+=num;
		printf("result111:%d \n",sum);
		sem_post(&sem2);
	}
	printf("result:%d \n",sum);
	return NULL;
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值