首页 > 系统相关 >Linux:线程同步机制(互斥锁、读写锁、条件变量、信号量详细分析总结)

Linux:线程同步机制(互斥锁、读写锁、条件变量、信号量详细分析总结)

时间:2024-08-11 18:26:39浏览次数:14  
标签:线程 信号量 互斥 cond pthread mutex

目录速览

1、互斥锁

线程同步机制是多线程协调工作的关键和基础,即控制对共享资源的先后访问(线程同步);常见的线程同步机制包括:互斥锁、读写锁、条件变量和信号量等四种方法。本文将介绍互斥锁的使用。

(1)What(什么是互斥锁)

本质是一个对象,实现对临界区的串行访问(互斥访问)

(2)Why(互斥锁的用途)

互斥锁的用途:实现多线程对共享资源的先后访问

(3)How(如何使用互斥锁)

在Linux中,使用互斥锁机制对共享资源的先后访问顺序如下:定义互斥锁对象、初始化互斥锁对象、关锁、开锁、销毁锁…

step01:定义互斥锁

相当于创建互斥量对象

pthread_mutex mutex;

step02:初始化互斥锁

相当于初始化对象

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 参数mutex:待初始化的互斥量对象
  • 参数attr:用于指定互斥锁的属性。如果为 NULL,则使用默认属性
  • 返回值:初始化成功返回0,否则初始化失败

step03:关锁

本质是调用操作互斥锁对象mutex的一个关锁函数,其它线程在访问该共享资源时,如果互斥锁变量属于关锁状态,则将会被阻塞

int pthread_mutex_lock(pthread_mutex_t  *mutex);
  • 参数mutex:互斥锁对象的地址
  • 返回值:成功关锁(表示成功获取互斥锁变量)返回0,否则返回非0

Note:当锁处于开状态时,调用该函数会改变mutex的状态为关,并记录哪个线程对它上的锁
当锁处于关状态时,调用该函数会失败,这些调用失败的线程都会阻塞在这把互斥锁对象上

step04:开锁

本质时调用操作互斥锁对象mutex的一个开锁函数,当获取该互斥锁的对象完成了对共享资源的访问时调用

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 参数mutex:被开始的互斥锁对象
  • 返回值:0表示开锁成功

step05:销毁锁

本质就是释放互斥锁对象,将资源归还给操作系统

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 参数mutex:被销毁的互斥锁对象
  • 返回值:0表示销毁成功

(4)代码实践

可直接在Linux平台编译和运行

#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
pthread_mutex_t  mutex;
vector<int> que;
void * threadProduce(void *arg)
{
	int i = 0;
	while(1){
		
		pthread_mutex_lock(&mutex);
		que.push_back(i);
		
		
		cout<<"push: "<<i<<"(Length is "<<que.size()<<")"<<endl;
		++i;
		pthread_mutex_unlock(&mutex);
		sleep(1);
	}
	return NULL;
}
void * threadConsume(void *arg)
{
	while(1){
		pthread_mutex_lock(&mutex);
		if(que.size()!=0)
		{

			int iVal = que.back();
			que.pop_back();
			cout<<"pop: "<<iVal<<"(Length is "<<que.size()<<")"<<endl;
		}
		pthread_mutex_unlock(&mutex);
		sleep(2);
	}
	return NULL;
}
int main()

{
	
	pthread_mutex_init(&mutex,NULL);
	pthread_t tidPro, tidCom;
	pthread_create(&tidPro, NULL, threadProduce, NULL);
	pthread_create(&tidCom, NULL, threadConsume, NULL);
	pthread_join(tidPro, NULL);
	pthread_join(tidCom, NULL);
}

2、读写锁

(1)What(什么是读写锁)

与互斥锁类似,读写锁的本质也是一个类对象,读锁对象允许读线程对临界区的并行访问,而写线程对临界区只能互斥访问(换言之,读写锁是互斥锁的升级版)

(2)Why(读写锁的作用)

在保护共享资源一致性的基础上提高了并发读的性能(换言之,在实现线程同步的基础上提高了并发读的性能)

(3)How(如何使用读写锁)

step01:定义读写锁及类型

本质就是创建一个读写锁对象,并指定锁的类型是读锁还是写锁

pthread_rwlock_t rwlock;

step02:初始化读写锁对象

本质就是给读写锁对象赋初始值

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr = NULL);
  • 参数rwlock:要初始化的读写锁对象
  • 参数attr:指定读写锁的属性,通常传递 NULL 以使用默认属性
  • 返回值:0表示初始化成功

step03:写锁锁定临界区

本质就是调用函数修改读写锁对象的状态,写锁只允许线程互斥访问临界区,当获取失败时,对应的线程会阻塞在这个读写锁对象上

#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  • 参数rwlock:被上写锁的读写锁对象
  • 返回值:0表示上写锁成功

step04:读锁锁定临界区

本质就是调用函数修改读写锁对象的状态,读锁允许读线程共同访问临界区,当该锁处于写状态时,会导致上读锁失败,线程阻塞

#include <pthread.h>

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 参数rwlock:被上读锁的读写锁对象
  • 返回值:0表示上读锁成功

step05:解锁

本质还是调用函数修改读写锁的状态,解锁之后,该读写锁对象对临界区的管理将会解除

#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 参数rwlock:将要被释放的读写锁对象
  • 返回值:0表示释放读写锁成功

step06:销毁读写锁对象

本质就是释放读写锁占有的内存,以归还给操作系统

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 参数rwlock:被销毁的读写锁对象
  • 返回值:0表示销毁成功

(4)读写锁的特征

  • 如果是读锁锁定了临界区,那么可以并行读取临界区,因为读锁对读是共享的
  • 如果是写锁锁定了临界区,那么只能串行访问临界区,因为写锁对读和写都是独占的

3、条件变量

(1)What(什么是条件变量)

一种线程同步机制,在线程中以睡眠的方式等待某一条件的发生,当条件不成立时挂起线程,条件成立时唤醒线程

(2)Why(条件变量的作用)

通过条件的满足与否来控制线程的等待与运行,目的是实现对线程的等待与唤醒以保证多线程之间对共享资源的安全有序访问(即实现线程同步)

(3)How(如何使用条件变量实现线程同步)

条件变量通常和互斥锁结合使用

step01:定义条件变量

在Linux中,条件变量的本质是一个数据结构(不同的操作系统其本质可能有一点要求)

pthread_cond_t cond;

step02:初始化条件变量

本质是给条件变量赋初始值

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr=NULL);

参数cond:待初始化的条件变量
参数attr:条件变量的属性,不传代表使用默认参数
返回值:初始化成功返回0,初始化失败返回错误码

step03:阻塞线程

本质就是调用函数改变条件变量的状态,阻塞当前线程,直到条件变量被唤醒

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

参数cond:要等待的条件变量
参数mutex:与条件变量关联的互斥锁
返回值:成功返回0,失败返回错误码

#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

上述函数与 pthread_cond_wait 类似,但会在指定的超时时间timespec后自动唤醒

step04:唤醒线程

本质就是调用函数改变条件变量的状态,发送信号给条件变量,唤醒阻塞在当前条件变量下的一个线程

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);

参数cond:要发送信号的条件变量
返回值:成功返回0,失败返回错误码

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);

唤醒所有等待在该条件变量上的线程

step05:销毁条件变量

本质就是调用函数释放条件变量占用的内存空间

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
  • 参数cond:要销毁的条件变量
  • 返回值:成功返回0,失败返回错误码

说明:

在典型的使用场景中,线程在访问共享资源之前会先获取互斥锁,然后判断对应的条件变量是否满足,如果条件不满足,线程会调用条件变量的等待操作,并释放互斥锁进入阻塞状态;当其它线程修改了共享数据并满足条件时,它会通过条件变量的信号或广播操作来唤醒等待该条件的线程。被唤醒的线程重新获取互斥锁,检查条件是否满足,如果条件满足就继续执行临界区的操作。

(4)生产者-消费者模型

使用互斥锁和条件变量实现生产者-消费者模型。生产者-消费者模式是一种常见的并发编程模型,用于解决生产者和消费者之间的协作和同步问题。

A.What(什么是生产者-消费者模型)

一种常见的并发编程模型。模型包括三个重要元素:生产者、消费者和共享缓冲区。生产者不断生产数据,如果共享缓冲区已满,生产者会等待,直到缓冲区有空闲空间。消费者不断从缓冲区中获取数据,如果缓冲区为空,消费者会等待,直到有新的数据被生产者放入缓冲区。

B.Why(该模型的作用)

  • 解耦了生产者和消费者的操作,使它们可以独立地运行
  • 提高了系统的并发性和效率,生产者和消费者可以同时工作,不必相互等待

C.How(如何使用条件变量和互斥锁实现该模型)

下列示例中,生产者可以无限生产,消费者需要判断缓冲区是否有产品可以消费。

全局变量

pthread_cond_t cond; //条件变量,用于控制消费者线程
pthread_mutex_t mutex; //互斥锁
sturct Node * head = NULL; //缓冲区

生产者线程

void * produce(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		//TODO:向head中添加数据节点
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(&cond); //唤醒消费线程
	}
	return NULL;
}

消费者线程

void * consume(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		while(head == NULL) //不满足消费条件,所以阻塞
		{
			pthread_cond_wait(&cond, &mutex);
		}
		pthread_mutex_unlock(&mutex);
	}
	return NULL;
}

main函数

void main()
{
	pthread_mutex_init(&mutex);
	pthread_cond_init(&cond);
	pthread_t consumer[5], producer[5];
	for(int i = 0; i<5; ++i)
	{
		pthread_create(&consumer[i], NULL, consume, NULL);
		pthread_create(&producer[i], NULL, produce, NULL);
	}
	//TODO:释放工作
}

4、信号量

(1)What(什么是信号量)

提供一种计数器的方式控制对共享资源的访问;当计数器大于0时,请求资源成功并计数器-1;当计数器小于0时,线程阻塞,等待其它线程执行signal(V操作)唤醒它

(2)Why(信号量的作用)

  • 实现线程的同步与互斥:通过信号量的设计,可以实现对共享资源的串行访问
  • 实现线程的等待与通知机制:当信号量小于0时,当前线程将被阻塞;当信号量大于0时,会唤醒一个阻塞在信号量上的线程

(3)How(如何使用信号量实现线程同步)

step01:创建信号量

可以看作是创建一个信号量对象

sem_t sem; 

step02:初始化信号量

可以看作是对信号量对象的一个初始化,这一过程会给信号量的计数器赋予一个初始值

int sem_init(sem_t *sem, int pshared, unsigned int value); 
  • 参数sem:被初始化的信号量对象
  • 参数pshared:默认为0,表示信号量用于线程同步;其它表示信号量用于进程同步
  • 参数value:表示信号量的数量,常用用于表示共享资源的数量
  • 返回值:成功返回0,失败返回-1,并设置错误码

step03:请求资源

请求获取共享资源,此时信号量的计数器减1;如果信号量小于1,请求失败,线程阻塞,直到信号量满足条件时解除阻塞

int sem_wait(sem_t *sem); 
  • 参数sem:请求共享资源,如果sem中的计数器大于0,则请求成功,否则线程阻塞
  • 返回值:成功返回0,失败返回-1,并设置错误码

step04:释放资源

释放共享资源,此时信号量的计数器加1,此时会唤醒一个等待该共享资源的线程

int sem_post(sem_t *sem);

  • 参数sem:释放共享资源,sem中的计数器+1
  • 返回值:成功返回0,失败返回-1,并设置错误码
    step05:销毁信号量

本质就是释放信号量对象的内存空间

int sem_destroy(sem_t *sem); 
  • 参数sem:将要被销毁的信号量对象
  • 返回值:成功返回0,失败返回-1,并设置错误码

(4)代码实例

以下代码是对共享资源的互斥访问,共享资源的个数为5

#include <semaphore.h>
#include <unistd.h>
#include <iostream>
#include <pthread.h>
using namespace std;
pthread_mutex_t mutex;
sem_t semProc;
sem_t semComu;
struct Node{
	int iVal;
	Node * ptrNext;
	static int iSize;
};
int Node::iSize = 0;
Node * head = NULL;
void *produce(void  *arg){
	int i = 0;
	while(1){
		sem_wait(&semProc);
		pthread_mutex_lock(&mutex);
		int iVal = i;
		Node *node = new Node;
		node->iVal = iVal;
		node->ptrNext = head->ptrNext;
		head->ptrNext = node;
		Node::iSize++;
		cout<<"Producing "<<iVal<<"("<<Node::iSize<<")"<<endl;
		pthread_mutex_unlock(&mutex);
		sem_post(&semComu);
		++i;
		sleep(1);
	}
}
void *consume(void *arg){
	while(1){
		sem_wait(&semComu);
		pthread_mutex_lock(&mutex);
		if(head->ptrNext!=NULL)
		{
			cout<<"Comsuing "<<head->ptrNext->iVal;
			Node *ptr = head->ptrNext;
			head->ptrNext = ptr->ptrNext;
			Node::iSize--;
			delete ptr;
			cout<<"("<<Node::iSize<<")"<<endl;
			ptr = 0;
		}
		pthread_mutex_unlock(&mutex);
		sem_post(&semProc);
		sleep(2);
	}
	return NULL; 
}
int main()
{
	head = new Node; 
	pthread_t tidProc,  tidComu; 
	sem_init(&semProc, 0, 5); 
	sem_init(&semComu, 0, 0); 
	pthread_create(&tidProc,  NULL,  produce,  NULL); 
	pthread_create(&tidComu, NULL, conmuse, NULL); 
	pthread_join(tidProc, NULL); 
	pthread_join(tidComu, NULL); 
	return 0; 
}

标签:线程,信号量,互斥,cond,pthread,mutex
From: https://blog.csdn.net/qq_42279379/article/details/140999342

相关文章

  • 多线程复习总结
     1基本概念1什么是进程什么是线程进程:是程序执行一次的过程,他是动态的概念,是资源分配的基本单位。一个应用程序(1个进程是一个软件)。线程:一个进程可以有多个线程,线程是cpu调度的单位,一个进程中的执行场景/执行单元。对于java程序来说,当在DOS命令窗口中输入:javaHelloWorld回......
  • 单例模式-饿汉、懒汉(线程安全、双重检查锁定)、静态内部类、枚举
    1饿汉式这种方式是最简单的单例模式实现,它在类加载的时候就创建了单例对象。优点实现简单线程安全缺点无论是否使用,都会加载单例类并创建单例对象publicclassSingleton{privatestaticfinalSingletonINSTANCE=newSingleton();privateSingleton(){......
  • 【Redis进阶】Redis单线程模型和多线程模型
    目录单线程为什么Redis是单线程处文件事件理器的结构文件处理器的工作流程总结文件事件处理器连接应答处理器命令请求处理器命令回复处理器多线程为什么引入多线程多线程架构多线程执行流程关于Redis的问题Redis为什么采用单线程模型Redis为什么要引入多线程呢......
  • Linux C++ 多线程编程
    LinuxC++多线程编程参考教程:c++:互斥锁/多线程的创建和unique_lock<mutex>的使用_mutex头文件vc++-CSDN博客1.编写unique_mutex1.1创建文件夹通过终端创建一个名为unique_mutex的文件夹以保存我们的VSCode项目,在/unique_mutex目录下打开vscode。rosnoetic@rosnoetic-Virt......
  • Python和多线程(multi-threading)
    在Python中,实现并行处理的方法有几种,但由于Python的全局解释器锁(GIL,GlobalInterpreterLock)的存在,传统意义上的多线程(使用threading模块)并不总能有效利用多核CPU来实现真正的并行计算。GIL确保任何时候只有一个线程在执行Python字节码。不过,仍然有几种方法可以绕过这个限制,......
  • Java - 多线程
    三种实现方式常用成员方法1.线程name默认“Thread-”+"序号"2.可以通过重写构造方法在创建时给线程命名线程的生命周期与状态同步代码块格式synchronized(锁对象){操作共享数据的代码}1.锁对象随机,但只要是有static修饰的唯一对象,一般写本类class文件,如MyTh......
  • C#多线程并发编程深度探索:解锁async、await、Task与lock等关键字的奥秘
    一、多线程介绍1.什么是多线程多线程是指在一个应用程序中同时执行多个线程的能力。每个线程都是独立运行的,拥有自己的执行路径和资源。多线程编程能够充分利用多核处理器的计算能力,提高应用程序的性能和响应性,特别是在处理耗时任务和并行计算时效果显著。在C#中,线程是程序......
  • 线程间的礼貌竞争:避免活锁现象
    线程间的礼貌竞争:避免活锁现象在多线程编程中,我们经常会遇到各种同步问题,其中一种比较特殊的情况被称为“活锁”(livelock)。活锁是一种递归情况,多个线程在执行过程中不断重复某段代码逻辑,这些线程通常是为了给其他线程让路而主动放弃执行机会。这就像两个在狭窄走廊上相遇......
  • 【JavaEE初阶】线程安全的集合类
    ......
  • 【IO】IPC通信机制函数(消息队列,共享内存,信号量集函数整理汇总)
            整理了一下IPC通信的函数,包括消息队列,共享内存,信号量集;信号量集的使用是在共享内存的基础上使用,函数太多啦,慢慢学吧cc,争取全部记住        其中在使用有关信号量集的函数的时候,进行简单的封装函数功能之后,再进行使用,会更加方便,在文章最后对信号量集的......