欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程同步图解 - 互斥量

程序员文章站 2022-05-04 17:10:06
...

线程同步问题广泛应用于多种场景下,特别是与网络数据收发等耗时操作有关的场景。线程的操作往往比较抽象,且大多运行在程序的后台,无法直观的观察其运行状态,因此,本文以图解的形式,为读者讲述线程同步的原理,并附以相关例程方便大家调试。

本文讲述互斥量(也叫互斥锁pthread_mutex_t)线程同步原理,互斥量作为比较简单的线程同步方式,在实际的并发问题中会经常碰到。

更多线程同步的例程见[公众号:断点实验室]的线程同步系列文章
线程同步图解 - 互斥量
线程同步图解 - 条件变量

例如,当我们在进程中通过几个线程以并发方式完成特定任务时,可能会出现这几个线程需要同时操作一个数据(即临界区资源,类似于抢火车票场景),若不对临界区资源进行必要的保护,将会操作数据错乱问题

互斥量用于保证对多个线程共享的临界区数据操作的完整性(原子性),即同一时刻只能有一个线程持有互斥量,而且只有这个线程可以对互斥量解锁,当无法获取互斥量时,其他线程进入睡眠等待状态。

1、线程同步原理

通过互斥量来实现线程对临界区资源同步访问的操作一般可分为三步(没错,操作步骤和把大象放进冰箱里是一样的),即

加锁 -> 访问临界区资源 -> 解锁

既然是线程同步图解,这里我们给出原理图来描述整个线程同步的过程

线程同步图解 - 互斥量
假设producer线程首先开始执行,producer线程同时锁定互斥量(图中红点表示互斥量),取得了对临界区资源的独占使用权,然后执行其线程函数操作临界区资源,最后释放互斥量,完成一轮对临界区资源的操作

consumer线程晚于producer线程启动,由于互斥量已被producer线程锁定,因此consumer线程将投入休眠状态,直到重新获得互斥量恢复执行

若consumer线程首先执行,情况与producer线程先开始执行的情况类似,同样是按照锁定互斥量,执行线程逻辑,释放互斥量这三个操作进行,若临界区资源满足条件,则执行consumer的线程逻辑,如图所示的从后台缓存队列中提取数据

若后台缓存队列为空,则consumer线程逻辑不满足执行条件,则直接释放互斥量,然后再次和producer线程重新展开对临界区资源独占的竞争,直到临界区资源满足线程执行逻辑为止

2 例程源码清单

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

static int val=0;//临界区资源,critical resource
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥锁

//生产者临界区资源处理函数
void producer_proc(int *cr){
	printf("producer_process_critical_resource++++++++++++++++++%d\n\n",val);
	*cr+=1;//处理临界区资源,做+1简单处理
}

//消费者临界区资源处理函数
void consumer_proc(int *cr){
	printf("consumer_process_critical_resource------------------%d\n\n",val);
	if(*cr>=10){
		*cr=0;//处理临界区资源,这里直接重置为0
	}
	
}

//生产者线程函数
void *producer_fun(void *p) {
	while(1){
		printf("producer_before_lock++++++++++++++++++++++++++++++++%d\n\n",val);
		pthread_mutex_lock(&lock);//取得互斥锁
		printf("producer_locked+++++++++++++++++++++++++++++++++++++%d\n\n",val);

		//处理临界区资源,做+1简单处理
		producer_proc(&val);

		printf("producer_unlock+++++++++++++++++++++++++++++++++++++%d\n\n",val);
		pthread_mutex_unlock(&lock);//释放互斥锁
		sleep(1);
	}//end for while
    return NULL;
}

//消费者线程函数
void *consumer_fun(void *p) {
	while(1){
		printf("consumer_before_lock--------------%d\n\n",val);
		pthread_mutex_lock(&lock);//取得互斥锁
		printf("consumer_locked-------------------%d\n\n",val);

		//处理临界区资源,这里直接重置为0
		consumer_proc(&val);

		printf("consumer_unlock-----------------------%d\n\n",val);
		pthread_mutex_unlock(&lock);
		sleep(1);
	}//end for while
    return NULL;
}

int main(int argc,char *argv[]) {
	pthread_t cid,pid;//线程id

	//创建消费者线程
	int ret=pthread_create(&cid,NULL,consumer_fun,NULL);
	if(ret!=0) {//检查线程创建结果
		printf("create consumer thread failed\n");
		exit(1);
	}

	//创建生成者线程
	ret=pthread_create(&pid,NULL,producer_fun,NULL);
	if(ret!=0) {//检查线程创建结果
		printf("create producer thread failed\n");
		exit(1);
	}

	//等待线程结束
	pthread_join(cid,NULL);
	pthread_join(pid,NULL);

	return 0;
} 

3 例程编译运行分析

下面给出例程的编译、运行以及结果的分析

3.1 例程编译

下面给出例程的编译脚步,编译过程非常简单,在源码及Makefile编译脚本目录执行[make]命令,即可完成例程编译,执行下面的操作即可开始例程的运行。

Makefile编译脚本

test: main.c
	gcc -o test -g3 main.c -l pthread

clean:
	rm test

代码编译运行

make
./test

3.2 例程运行结果

producer_before_lock++++++++++++++++++++++++++++++++4

producer_locked+++++++++++++++++++++++++++++++++++++4

consumer_before_lock--------------4

producer_process_critical_resource++++++++++++++++++4

producer_unlock+++++++++++++++++++++++++++++++++++++5

consumer_locked---------------------5

consumer_process_critical_resource------------------5

3.3 运行结果分析

从上面的运行结果可以看出

producer线程在已锁定互斥量的情况下(producer_lock),获得了对临界区资源的独占使用权

consumer线程也试图锁定互斥量(consumer_before_lock),但此时互斥量已被producer线程锁定,因此consumer线程将阻塞等待(consumer_lock未执行),直到互斥量被释放为止

producer线程继续执行自己的逻辑,处理临界区资源(producer_process_critical_resource)

producer线程执行完毕后,释放互斥量(producer_unlock)

因为此时等待互斥量的线程只有一个,因此consumer线程取得互斥量(consumer_lock)并恢复运行,然后执行自己的逻辑(consumer_process_critical_resource)

若此时存在多个线程同时等待互斥量,那么当互斥量被释放后,将出现多个线程竞争同一个互斥量的场景,最终哪个线程能够锁定互斥量将取决于内核的调度

若线程释放互斥量并以广播形式唤醒其他休眠中的线程,此时将引发惊群效应,即多个线程几乎同时醒来,但只有其中一个线程可以获得互斥量并执行自己的逻辑,其他未获得互斥量的线程只能继续进入休眠状态

惊群效应是应该尽力避免的,因为这种线程同步方式效率低,且浪费处理器资源及时间,惊群效应会在后续的内容中为大家讲解,如果有人感兴趣的话

由于线程的调度存在随机性,每次执行的时序可能不完全相同,因此读者在自己的环境中执行的结果可能和这里贴出来的结果不完全相同,这是正常的,即使是同一套代码每次执行的结果也有一定的随机性

4 互斥量同步方式的问题

采用互斥量的方式保护缓存队列临界区资源,若缓存队列为空,则consumer线程逻辑不满足执行条件,则直接释放互斥量并退出,然后再次和producer线程重新展开对临界区资源独占的竞争。

由于线程调度的随机性,若consumer线程在临界区资源不满足执行条件的情况下,再次获得优先执行权,那么线程逻辑会直接退出本次调用,这种情况浪费了宝贵的处理器资源及时间,因此,应该采用其他效率更高的同步方式来操作线程同步。

更多线程同步的例程见[公众号:断点实验室]的线程同步系列文章
线程同步图解 - 条件变量


// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 公众号:断点实验室
// 扫描二维码,关注更多优质原创,内容包括:音视频开发、图像处理、网络、
// Linux,Windows、Android、嵌入式开发等

线程同步图解 - 互斥量
相关标签: 线程同步