经典的并发控制模型
主要是练习mutex unique_lock conditional_variable
[[20 原子操作]]
一、互斥量
1 mutex 互斥量
mutex
是一种互斥的同步原语,用于保护共享资源的访问,确保在同一时间只有一个线程可以访问共享资源。通过对互斥量加锁和解锁,可以实现对共享资源的独占访问。
2 shared_mutex 共享互斥量
允许多个线程同时获取共享访问权限,适用于读多写少的场景。 要想实现共享的概念,可以使用与shared_lock
包装器搭配使用[[10 生产者-消费者模型(互斥量,锁,条件 变量)#4 shared_lock]]。
二、互斥量包装器
1 lock_guard
轻量级的互斥量包装器,用于自动获取互斥锁并在作用域结束时自动释放锁。它适用于临界区的简单互斥保护,没有额外的灵活性
2 unique_lock 独占锁
unique_lock
是一个灵活的互斥量包装器,提供了更高级别的互斥操作。它是基于互斥量的封装,可以通过构造函数或成员函数的方式对互斥量进行加锁和解锁。与普通的 lock()
和 unlock()
不同,unique_lock
在创建时可以选择性地对互斥量进行加锁,也可以在任何时候手动加锁或解锁。mtx.lock() 和 mtx.unlock()
;
- 自动加锁和解锁
- 延迟加解锁,允许在需要时手动加锁和解锁,如加锁、解锁、尝试加锁等:
lock.lock();
,lock.unlock();
,lock.try_lock();
- 可移动性,
std::unique_lock
对象可以被移动,因此可以在不同线程之间传递所有权。这对于实现资源所有权的转移非常有用。
3 scoped_lock
C++17引入的互斥量包装器,可以同时锁定任意数量的互斥量,还可以避免死锁,因为它使用了无死锁的算法来获取多个互斥量。它会按照参数的顺序来进行加锁,并确保在作用域结束时以相反的顺序释放锁。
不支持手动的加锁和解锁操作。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
void Worker()
{
// 同时锁定mtx1和mtx2
std::scoped_lock lock(mtx1, mtx2);
// 执行任务
std::cout << "Worker: Doing some work..." << std::endl;
}
int main()
{
std::thread workerThread(Worker);
// 等待工作线程完成
workerThread.join();
return 0;
}
4 shared_lock
共享锁的互斥量包装器,用于实现共享所有权的线程同步。它允许多个线程同时共享对互斥资源的访问,提高了并发性能。
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex mtx;
int sharedData = 0;
void Reader()
{
std::shared_lock lock(mtx); // 获取共享锁
// 读取共享数据
std::cout << "Reader: Shared data = " << sharedData << std::endl;
}
void Writer()
{
std::unique_lock lock(mtx); // 获取独占锁
// 修改共享数据
sharedData += 1;
std::cout << "Writer: Incremented shared data" << std::endl;
}
int main()
{
std::thread readerThread1(Reader);
std::thread readerThread2(Reader);
std::thread writerThread(Writer);
readerThread1.join();
readerThread2.join();
writerThread.join();
return 0;
}
三、条件变量
条件变量(Condition Variable)是一种线程同步的机制,用于线程间的等待和通知。条件变量允许一个或多个线程在满足特定条件之前进行等待,一旦条件满足,其他线程可以通知等待的线程继续执行。
1、使用步骤
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
std::string flag("A");
void PrintA()
{
while(true) {
std::unique_lock<std::mutex> lck(mtx);
while (flag == "B") {
cv.wait(lck);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "A" << std::endl;
flag = "B";
cv.notify_all();
}
}
void PrintB()
{
while(true) {
std::unique_lock<std::mutex> lck(mtx);
while (flag == "A") {
cv.wait(lck); // cv.wait(lck, lambda)
}
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "B" << std::endl;
flag = "A";
cv.notify_all();
}
}
int main()
{
std::thread t1(PrintA);
std::thread t2(PrintB);
t1.join();
t2.join();
std::cin.get();
}
2, 调用 wait() 后等待过程
用于在等待条件满足时将线程置于等待状态,并释放所持有的互斥锁。
-
线程首先获取与条件变量关联的互斥锁(
std::mutex
)的所有权,这是通过调用std::unique_lock
构造函数来实现的。 -
然后,线程进入等待状态,并且该线程的执行被暂时阻塞,等待条件满足或被唤醒。
-
在等待状态下,线程会自动释放互斥锁,允许其他线程获得锁并对共享数据进行操作。
-
当条件变量的
notify_one()
或notify_all()
函数被调用并且相应的通知被发送时,线程会从等待状态中被唤醒。 -
被唤醒的线程尝试重新获得互斥锁的所有权。一旦成功获得锁,线程将退出
wait()
函数并继续执行后续的代码。
注意: 在进行条件判断的时候使用的是while而不是if,这是为了防止虚假唤醒(Spurious Wakeup) ,当线程被唤醒时,它会再次检查条件,如果条件不满足,则继续等待。这样可以确保只有在条件真正满足时才会跳出循环继续执行后续的代码。通常建议始终使用 while
循环来判断条件。
在wait中使用lambda时,只有当返回 false
时,线程会进入等待状态;当谓词返回 true
时,线程会继续执行后续的代码。这与使用while是相反的。
3,什么时候会发生虚假唤醒
虚假唤醒的发生是由操作系统和硬件等因素引起的,具体原因可能包括但不限于以下情况:
-
优化和调度:操作系统和硬件可能会进行一些优化和调度策略,导致等待线程在没有明确通知的情况下被唤醒。这种唤醒是不可预测的,可能是为了提高性能或满足其他系统需求。
-
中断和信号:在某些情况下,操作系统可能会向线程发送中断或信号,以响应某些事件或条件的发生。这可能会导致线程被唤醒,即使没有显式的通知也会发生虚假唤醒。文章来源:https://www.toymoban.com/news/detail-459036.html
虚假唤醒的发生是无法完全避免的,因此在使用条件变量进行线程同步时,需要进行适当的防护措施。常见的防护措施包括:文章来源地址https://www.toymoban.com/news/detail-459036.html
- 使用
while
循环进行条件判断 - 结合条件变量和互斥量
- 使用谓词进行条件判断:在等待过程中,使用带有谓词的
cv.wait()
函数进行条件判断。
到了这里,关于从生产者-消费者模型中学习互斥量,锁,条件变量的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!