【Linux C | 多线程编程】互斥量、信号量、条件变量对比总结

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
⏰发布时间⏰:

本文未经允许,不得转发!!!


在这里插入图片描述

🎄一、概述

在Linux系统多线程编程过程中,常常需要处理多个线程同步的问题。在处理多线程同步问题时,最常见的三种方式是:互斥量、信号量、条件变量,关于这三种方式的使用,在前面使用了四篇文章进行介绍和总结了,文章链接如下,感兴趣的自行取用:

【Linux C | 多线程编程】线程同步 | 互斥量(互斥锁)介绍和使用
【Linux C | 多线程编程】线程同步 | 条件变量(万字详解)
【Linux C | 多线程编程】线程同步 | 条件变量 的 使用总结
【Linux C | 多线程编程】线程同步 | 信号量(无名信号量) 及其使用例子

本文主要是对互斥量、信号量、条件变量三种使用方式的对比和总结,可以帮助我们对这三种线程同步方式的用法加深理解。

下文会经常提到临界区,所谓临界区,是指同一时间只能容许一个进程进入的一系列操作。


在这里插入图片描述

🎄二、互斥量与信号量

关于互斥量与信号量,比较形象的比喻是:互斥量是一间厕所,每次只允许一人访问; 信号量类似一个公共卫生间,里面的4间(多个)厕所,可以同时允许4个人访问。

互斥量的本质是一把锁,其作用是对临界区进行安全地访问(独占访问),当多个线程访问临界区时,保证同一时刻只有一个线程访问临界区。互斥量有以下几个特点:
1、互斥量只有两种状态,已上锁、已解锁;
2、互斥量总是由给它上锁的线程解锁;
3、互斥量的访问方式是独占的,只有互斥量上锁了,其他线程就只能等待其访问完解锁,以此来保证一个时刻只有一个线程占有互斥量。

信号量是与某些资源相关联的,信号量初始化的信号量值表示资源个数。信号量值初始化为1则表示是二值信号量,若初始化的信号量值大于 1 则是计数信号量。信号量的特点如下:
1、二值信号量的取值只有两个:0、1;计数信号量的取值大于2个;
2、信号量可以在一个线程等待(减一),另一个线程发布(加一);也可以是同一个线程等待(减一)并发布(加一)。

✨2.1 互斥量与信号量的相似之处

互斥量和信号量有相同的地方,特别是二值信号量与互斥量甚至可以替换着使用,在Linux早期,就经历过将信号量替换成互斥量的过程,下图来源于网络,说明了这个替换过程:
在这里插入图片描述
比起互斥量,信号量先被设计和实现出来,且二值信号量可以起到互斥作用,所以早期的Linux代码使用了信号量来访问临界区。而互斥量最早是在2.6.16内核中由Red Hat Enterprise Linux的资深内核专家Ingo Molnar设计和实现的,之前的内核代码使用信号量也没有问题,但是互斥量在锁争用激烈的情况下,互斥量比信号量执行速度更快,可扩展性更好。

下面是使用互斥量访问临界区的示例代码:

// 08_mutex_test.c
// gcc 08_mutex_test.c -lpthread
#include <stdio.h>
#include <pthread.h>
#include <sys/syscall.h>
int g_Count = 0;
pthread_mutex_t g_mutex;
void *func(void *arg)
{
	int i=0;
	for(i=0; i<10000000; i++)
	{
		pthread_mutex_lock(&g_mutex);
		g_Count++;
		pthread_mutex_unlock(&g_mutex);
	}
	return NULL;
}

int main()
{
	pthread_mutex_init(&g_mutex, NULL);
	// 创建4个线程
	pthread_t threadId[4];
	int i=0;
	for(i=0; i<4; i++)
	{
		pthread_create(&threadId[i], NULL, func, NULL);
	}

	for(i=0; i<4; i++)
	{
		pthread_join(threadId[i],NULL);
		printf("join threadId=%lx\n",threadId[i]);
	}
	printf("g_Count=%d\n",g_Count);
	
	pthread_mutex_destroy(&g_mutex);
	
	return 0;
}

下面是使用信号量访问临界区的示例代码,可以与上一份代码对比,就可以知道互斥量和信号量的用于互斥时是相同的:

// 10_sem_mutex.c
// gcc 10_sem_mutex.c -l pthread
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int g_Count = 0;
sem_t g_sem;
void *func(void *arg)
{
	int i=0;
	for(i=0; i<10000000; i++)
	{
		sem_wait(&g_sem);
		g_Count++;
		sem_post(&g_sem);
	}
	return NULL;
}

int main()
{
	sem_init(&g_sem, 0, 1);
	// 创建4个线程
	pthread_t threadId[4];
	int i=0;
	for(i=0; i<4; i++)
	{
		pthread_create(&threadId[i], NULL, func, NULL);
	}

	for(i=0; i<4; i++)
	{
		pthread_join(threadId[i],NULL);
		printf("join threadId=%lx\n",threadId[i]);
	}
	printf("g_Count=%d\n",g_Count);
	
	sem_destroy(&g_sem);
	
	return 0;
}

✨2.2 互斥量与信号量的不同之处

互斥量与信号量的不同之处也很多,但主要的有两点:
1、互斥量总是在同一个线程加锁、解锁;信号量可以在一个线程等待(减一),另一线程发布(加一)。
2、互斥量只有两个状态:已加锁、已解锁;信号量可以初始为大于1的值,与多个资源关联。

关于信号量可以在不同线程等待、发布的例子,决定在其与条件变量对比时在介绍。下面是信号量关联多个资源的例子:

// 10_sem_multiple.c
// gcc 10_sem_multiple.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>

#define  TOILET_NUM		4
#define  PEOPLE_NUM 	8

int 			toilets[TOILET_NUM] = {0,};		// 4个蹲厕
pthread_mutex_t toilet_mutex = PTHREAD_MUTEX_INITIALIZER;	// toilets 的互斥量
sem_t 			g_sem;

int getToilet()
{
	int i=0;
	for(i=0; i<TOILET_NUM; i++)
	{
		if(toilets[i] == 0)
			break;
	}
	return i;
}

int sem_value()
{
	int semvalue = 0;
	sem_getvalue(&g_sem, &semvalue);
	return semvalue;
}

// 上厕所线程
void *going_to_the_toilet(void *arg)
{
	int id = *((int*)arg);
	int count = 2;
	while(count-->0){
		printf("线程[%d] 等待厕所,厕所数量=%d\n",id, sem_value());
		sem_wait(&g_sem);
		pthread_mutex_lock(&toilet_mutex);	// 厕所有多个线程访问,加锁
		int i = getToilet();
		if(getToilet()==TOILET_NUM){
			printf("线程[%d], No toilet\n",id);
		}
		else{
			toilets[i] = 1;		// 表示进入该厕所
			printf("线程[%d] 进入厕所[%d], 即将工作 2s\n",id, i);
			pthread_mutex_unlock(&toilet_mutex); // 上厕所前先释放锁,让其他人可以访问厕所资源
			sleep(2);		// 正在上厕所...
			pthread_mutex_lock(&toilet_mutex);
			toilets[i] = 0;
			printf("线程[%d] 完成工作,厕所[%d]空闲\n",id, i);
		}
		pthread_mutex_unlock(&toilet_mutex);
		sem_post(&g_sem);
		sleep(1);	// 释放资源后,休眠1秒,确保资源让出去
	}
	return NULL;
}

int main()
{
	sem_init(&g_sem, 0, TOILET_NUM);// 初始化信号量值为4
	
	// 创建线程
	pthread_t people_thid[PEOPLE_NUM];
	int i=0, num[PEOPLE_NUM]={0,};
	for(i=0; i<PEOPLE_NUM; i++)
	{
		num[i] = i;
		pthread_create(&people_thid[i], NULL, going_to_the_toilet, &num[i]);
	}
	
	// 等待线程
	for(i=0; i<PEOPLE_NUM; i++)
	{
		pthread_join(people_thid[i], NULL);
	}
	
	sem_destroy(&g_sem);
	return 0;
}

运行结果这里就不展示了,可以看看上篇文章,关于信号量的。


在这里插入图片描述

🎄三、信号量与条件变量

条件变量用来阻塞一个线程,直到某条件满足为止。
信号量也可以使一个线程陷入阻塞(等待信号量),直到另一个线程发布信号量。
仔细看上面两句话,分别描述了条件变量、信号量的功能,而且这两个功能看起来很类似,下面通过使用条件变量、信号量解决“生产者-消费者”的同步问题来了解条件变量、信号量使用上的区别。

下面是使用条件变量的代码:

// 09_producer_consumer_cond.c
// gcc 09_producer_consumer_cond.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include "linux_list.h"

#define  COMSUMER_NUM	2

typedef struct _product
{
	struct list_head list_node;
	int product_id;
}product_t;

struct list_head productList;// 头结点
pthread_mutex_t product_mutex = PTHREAD_MUTEX_INITIALIZER;	// productList 的互斥量
pthread_cond_t  product_cond = PTHREAD_COND_INITIALIZER;	// 条件变量

// 生产者线程,1秒生成一个产品放到链表
void *th_producer(void *arg)
{
	int id = 0;
	while(1)
	{
		product_t *pProduct = (product_t*)malloc(sizeof(product_t));
		pProduct->product_id = id++;
		
		pthread_mutex_lock(&product_mutex);
		list_add_tail(&pProduct->list_node, &productList);
		pthread_cond_signal(&product_cond);
		pthread_mutex_unlock(&product_mutex);
		
		sleep(1);
	}
	
	return NULL;
}

// 消费者线程,1秒消耗掉一个产品
void *th_consumer(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&product_mutex);
		while(list_empty(&productList)) // 条件不满足
		{
			pthread_cond_wait(&product_cond, &product_mutex);
		}
		// 不为空,则取出一个
		product_t* pProduct = list_entry(productList.next, product_t, list_node);// 获取第一个节点
		printf("consumer[%d] get product id=%d\n", *((int*)arg), pProduct->product_id);
		list_del(productList.next); // 删除第一个节点
		free(pProduct);
		pthread_mutex_unlock(&product_mutex);
	}
	return NULL;
}

int main()
{
	INIT_LIST_HEAD(&productList);	// 初始化链表
	
	// 创建生产者线程
	pthread_t producer_thid;
	pthread_create(&producer_thid, NULL, th_producer, NULL);
	
	// 创建消费者线程
	pthread_t consumer_thid[COMSUMER_NUM];
	int i=0, num[COMSUMER_NUM]={0,};
	for(i=0; i<COMSUMER_NUM; i++)
	{
		num[i] = i;
		pthread_create(&consumer_thid[i], NULL, th_consumer, &num[i]);
	}
	
	// 等待线程
	pthread_join(producer_thid, NULL);
	for(i=0; i<COMSUMER_NUM; i++)
	{
		pthread_join(consumer_thid[i], NULL);
	}
	return 0;
}

下面是使用信号量的代码:

// 10_producer_consumer_sem.c
// gcc 10_producer_consumer_sem.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <errno.h>
#include "linux_list.h"

#define  COMSUMER_NUM	2

typedef struct _product
{
	struct list_head list_node;
	int product_id;
}product_t;

struct list_head productList;// 头结点
pthread_mutex_t product_mutex = PTHREAD_MUTEX_INITIALIZER;	// productList 的互斥量
sem_t 			g_sem;

// 生产者线程,1秒生成一个产品放到链表
void *th_producer(void *arg)
{
	int id = 0;
	while(1)
	{
		product_t *pProduct = (product_t*)malloc(sizeof(product_t));
		pProduct->product_id = id++;
		
		pthread_mutex_lock(&product_mutex);
		list_add_tail(&pProduct->list_node, &productList);
		pthread_mutex_unlock(&product_mutex);
		sem_post(&g_sem);
		
		sleep(1);
	}
	
	return NULL;
}

// 消费者线程,1秒消耗掉一个产品
void *th_consumer(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&product_mutex);
		while(list_empty(&productList)) // 条件不满足
		{
			pthread_mutex_unlock(&product_mutex);
			sem_wait(&g_sem);
			pthread_mutex_lock(&product_mutex);
		}
		// 不为空,则取出一个
		product_t* pProduct = list_entry(productList.next, product_t, list_node);// 获取第一个节点
		printf("consumer[%d] get product id=%d\n", *((int*)arg), pProduct->product_id);
		list_del(productList.next); // 删除第一个节点
		free(pProduct);
		pthread_mutex_unlock(&product_mutex);
	}
	return NULL;
}

int main()
{
	INIT_LIST_HEAD(&productList);	// 初始化链表
	sem_init(&g_sem, 0, 1);			// 初始化信号量
	
	// 创建生产者线程
	pthread_t producer_thid;
	pthread_create(&producer_thid, NULL, th_producer, NULL);
	
	// 创建消费者线程
	pthread_t consumer_thid[COMSUMER_NUM];
	int i=0, num[COMSUMER_NUM]={0,};
	for(i=0; i<COMSUMER_NUM; i++)
	{
		num[i] = i;
		pthread_create(&consumer_thid[i], NULL, th_consumer, &num[i]);
	}
	
	// 等待线程
	pthread_join(producer_thid, NULL);
	for(i=0; i<COMSUMER_NUM; i++)
	{
		pthread_join(consumer_thid[i], NULL);
	}
	
	sem_destroy(&g_sem);
	return 0;
}

运行结果是两份代码都可以使用“生产者-消费者”模式之间的线程同步,但有以下区别:
1、条件变量的等待函数要求传入互斥量作为参数,所以条件变量必须和互斥量同时使用;信号量没有类似的要求,虽然访问临界区时会用到互斥量,但这不是信号量要求的。
2、条件变量要求“①查询条件、②阻塞等待”是一个原子操作;信号量则允许“①查询条件、②阻塞等待”不是一个原子操作。

条件变量之所以要求“①查询条件、②阻塞等待”是一个原子操作,是因为判断条件和pthread_cond_wait之间被打断的话,可能在判断条件之后,调用pthread_cond_wait之前,条件变量就被唤醒了,导致该线程一直等待下去。

信号量之所以不需要要求“①查询条件、②阻塞等待”是一个原子操作,是因为它是计数的,如果判断完条件之后,调用sem_wait(&g_sem);之前,信号量发布(加一)了,那么调用sem_wait(&g_sem);时直接不需要等待,直接继续执行,不存在唤醒丢失的情况。总之,条件变量唤醒时如果没有线程等待在该条件变量上,那么信号将丢失;而信号量有计数值,每次信号量post操作都会被记录。


在这里插入图片描述

🎄四、互斥量与条件变量

互斥量与条件变量不像前面那样有相似的功能,更多的是互补的关系。

互斥量主要功能是使各个线程独占地访问临界区。
条件变量的功能是提供线程阻塞等待、其他线程唤醒的同步操作。但是没有提供安全访问临界区的功能,所以使用条件变量都需要互斥量一同使用。


在这里插入图片描述

🎄五、总结

👉本文总结了“互斥量与信号量使用的区别”、“信号量与条件变量使用的区别”、互斥量与条件变量的关系,并给出部分例子解析这些差异的存在。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

参考:
https://blog.csdn.net/tugouxp/article/details/68951576

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wkd_007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值