zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++ 锁机制以及常用方法(理论+实践)

C++方法 实践 常用 以及 机制 理论
2023-09-11 14:20:00 时间

整理了学习过程的一些笔记,觉得有帮助的三连一下呗!
😄From 忆_恒心

1、几种典型的所

1.1 读写锁

  • 多个读者可以同时进行读
  • 写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
  • 写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)

1.2 互斥锁

一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁

1.3 条件变量

互斥锁一个明显的缺点是他只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

图解操作系统里面 妈妈叫孩子吃饭的例子

1.4 自旋锁

如果进线程无法取得锁,进线程不会立刻放弃CPU时间片,而是一直循环尝试获取锁,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

2、易混点

2.1 互斥锁和条件变量锁通常一起使用

条件变量

实现多个线程间的同步,当条件不满足时,相关线程一直被阻塞,直到某种条件出现,这些线程才会被唤醒

互斥锁

多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁)

🔥简单的来说:

互斥锁是线程间互斥的机制,条件变量则是同步机制。

在C++的实现中

 condition_variable cv;// 条件变量 
 mutex myMutex;// 互斥锁

2.1.1举个🌰

采用三个线程交替打印ABC

知识储备

mutex 实现互斥锁

unique_lock 上独占互斥所

condition_variable 条件变量

通知

notify_one通知一个等待的线程

notify_all 通知所有等待的线程

等待

wait 阻塞当前线程,直到条件变量被唤醒

wait_for 阻塞当前线程,直到条件变量被唤醒,或到指定时长后

wait_until 阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点

 #include<iostream>
 #include<thread>
 #include<mutex>
 #include<condition_variable>
 using namespace std;
 condition_variable cv;// 条件变量 
 mutex myMutex;// 互斥锁 
 int flag = 0;
void printA(){
	//与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。 
 	unique_lock<mutex> lk(myMutex); 
 	for(int i = 0; i < 10; ++i){
 		if(flag != 0){
 			cv.wait(lk);
		 }
		flag = 1;
		cout<<"线程一打印 "<<endl;
		cout<<i<<endl;
		cv.notify_all();
	 }
	 cout<<"线程一打印完毕"<<endl;
 }
 
void printB(){
	unique_lock<mutex> lk(myMutex);
	for(int i = 0; i< 10; ++i){
		if(flag !=1){
			cv.wait(lk);
		}
		flag = 2;
		cout<<"线程二打印 "<<endl;
		cout<<i<<endl;
		cv.notify_all();
	}
	cout<<"线程二打印完毕"<<endl;
}

void printC(){
	unique_lock<mutex> lk(myMutex);
	for(int i = 0; i< 10; ++i){
		if(flag !=2){
			cv.wait(lk);
		}
		flag = 0;
		cout<<"线程三打印 "<<endl;
		cout<<i<<endl;
		cv.notify_all();
	}
	cout<<"线程三打印完毕"<<endl;
}

int main(){
	thread th1(printA);
	thread th2(printB);
	thread th3(printC);
	
	th1.join();
	th2.join();
	th3.join();
	
	cout<<"main thread"<<endl; 
	return 0;
}

同时看到了小林的一篇好文 [C++并发编程之互斥锁和条件变量的性能比较](C++ 并发编程之互斥锁和条件变量的性能比较)

2.2 信号量

信号量其实是一个整形计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。

信号量也可以在线程间实现互斥同步:

互斥的方式:可保证任意时刻只有一个线程访问共享资源。

同步的方式:可保证线程A应在线程B之前执行。

信号量 通常也表示资源的数量,对应的变量是一个sem型。

由两个院子操作的系统函数来控制信号量

分别是:

P操作:将sem1,相减后,如果sem<0,则进程/线程进入阻塞等待,否则继续,表明P操作可能会阻塞。

V操作:将sem1,相加后,如果sem<=0,唤醒一个等待中的线程/进程,表明V操作不会阻塞。

2.2.1 举个栗子🌰:

image-20220317091754316

对于两个并发线程,互斥信号量的值仅取1. 0和-1三个值,分别表示:

  • 如果互斥信号量为1,表示没有线程进入临界区;
  • 如果互斥信号量为0, 表示有-个线程进入临界区;
  • 如果互斥信号量为-1,表示一个线程进入临界区,另一个线程等待进入。

3、总结

在并发编程中,如果资源数量比较少,采用条件变量和互斥量的情况比较多,值得注意的是条件变量一般用于同步问题,互斥量用于用于线程间互斥的机制。比如要保证一个资源一次只能被一个线程使用,用互斥量,而完成了使用需要执行某种顺序时候采用条件变量