😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
⏰发布时间⏰:
本文未经允许,不得转发!!!
目录
- 🎄一、概述
- 🎄二、互斥量与信号量
- ✨2.1 互斥量与信号量的相似之处
- ✨2.2 互斥量与信号量的不同之处
- 🎄三、信号量与条件变量
- 🎄四、互斥量与条件变量
- 🎄五、总结
🎄一、概述
在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