当前位置: 首页 > news >正文

C++扩展 --- 并发支持库(下)

C++扩展 --- 并发支持库(中)https://blog.csdn.net/Small_entreprene/article/details/149537183?fromshare=blogdetail&sharetype=blogdetail&sharerId=149537183&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link

4. lock_guard 

在 C++ 开发中,我们常常会遇到一些需要成对操作的场景,例如 newdeletemallocfreelockunlock 等。这些操作如果手动管理,很容易出现问题。比如,当程序在执行过程中抛出异常时,可能会导致某些操作未能正确执行,从而引发资源泄漏、死锁等一系列问题。手动管理这些操作的复杂性和潜在风险,使得我们迫切需要一种更加高效、安全的解决方案来简化资源管理。

RAII(Resource Acquisition Is Initialization,资源获取即初始化)正是为了解决这一问题而诞生的。它的核心思想是将资源的获取与对象的初始化绑定在一起,一旦获取到资源,就立即将其交给一个对象进行管理。这个对象的析构函数会自动负责资源的清理工作,例如释放内存或解锁。通过这种方式,资源的生命周期与对象的生命周期紧密绑定,无论程序是否发生异常,资源都能在对象析构时得到妥善处理,从而有效避免了资源泄漏和死锁等问题,大大提高了代码的健壮性和可维护性。

lock_guard 就是 C++11 提供的用于支持 RAII 方式管理互斥锁资源的类,能够有效防止因异常等原因导致的死锁问题。其大致原理类似于下面代码中的 LockGuard

#include <iostream> // 包含标准输入输出流库
#include <chrono>   // 包含时间相关功能
#include <thread>   // 包含线程功能
#include <mutex>    // 包含互斥锁功能
using namespace std;// LockGuard 是一个模板类,用于管理互斥锁,遵循 RAII 原则
template<class Mutex>
class LockGuard
{
public:// 构造函数:接受一个互斥锁的引用,并立即锁定该互斥锁LockGuard(Mutex& mtx) : _mtx(mtx) {_mtx.lock(); // 锁定互斥锁}// 析构函数:在对象生命周期结束时释放互斥锁~LockGuard() {_mtx.unlock(); // 解锁互斥锁}private:Mutex& _mtx; // 互斥锁的引用,确保与传入的互斥锁绑定
};

LockGuard 类中使用 Mutex& 是为了确保互斥锁对象的生命周期与 LockGuard 对象的生命周期紧密绑定。通过引用,LockGuard 直接绑定到传入的互斥锁对象上,而不是创建互斥锁的拷贝。这样可以避免因拷贝构造或赋值操作导致的潜在问题,因为标准库中的互斥锁(如 std::mutex)是不可拷贝的。

使用引用可以简化代码逻辑并提高性能。引用避免了不必要的拷贝操作,直接操作原始互斥锁对象,从而减少了资源开销。此外,引用的使用也使得 LockGuard 的语义更加清晰:它只是一个互斥锁的“管理者”,而不是互斥锁的所有者。

最后,使用引用可以防止一些常见的错误,例如 LockGuard 持有无效的互斥锁对象。如果使用指针,可能会出现指针指向的互斥锁对象被提前销毁的情况,导致 LockGuard 在析构时尝试对一个已经销毁的对象调用 unlock,从而引发未定义行为。而引用则保证了 LockGuard 始终绑定到一个有效的互斥锁对象上。

也就是说:成员变量内部使用引用,引用的成员变量必须在初始化列表进行初始化,初始化列表可以认为是该类定义的地方,还用实参用引用的才是外面哪一个锁,才是同一个锁!

lock_guard 的功能简单纯粹,仅支持通过 RAII 方式管理锁对象。它可以在构造时通过传入 adopt_lock_tadopt_lock 对象来管理已经加锁的锁对象。此外,lock_guard 类不支持拷贝构造。

int main()
{int x = 0;mutex mtx;auto Print = [&x, &mtx](size_t n) {{lock_guard<mutex> lock(mtx);//出了作用域自动解锁//LockGuard<mutex> lock(mtx);//mtx.lock();for (size_t i = 0; i < n; i++){++x;}//mtx.unlock();}};thread t1(Print, 1000000);thread t2(Print, 2000000);t1.join();t2.join();cout << x << endl;return 0;
}

lock_guard 在上述代码保护的是for循环,但是如果后续还有代码呢?

使用局部域:

	int x = 0;mutex mtx;auto Print = [&x, &mtx](size_t n) {{{lock_guard<mutex> lock(mtx);//出了作用域自动解锁//LockGuard<mutex> lock(mtx);//mtx.lock();for (size_t i = 0; i < n; i++){++x;}}//mtx.unlock();for (size_t i = 0; i < 10; i++){x;}}};

锁定构造函数

explicit lock_guard(mutex_type& m);

作用:构造一个 lock_guard 对象时,立即锁定传入的互斥锁 m

mutex_type& m:一个互斥锁的引用,通常是 std::mutex 或其他派生自 std::mutex 的类。

内部:在构造函数中调用 m.lock(),锁定互斥锁。在析构函数中调用 m.unlock(),释放互斥锁。

std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx); // 锁定互斥锁
// 在这里可以安全地访问共享资源

采用锁构造函数

lock_guard(mutex_type& m, adopt_lock_t tag);//adopt --- 领养

作用:构造一个 lock_guard 对象时,假设传入的互斥锁 m 已经被锁定,lock_guard 不会再次锁定它。

mutex_type& m:一个互斥锁的引用。

adopt_lock_t tag:一个特殊的标记,表示互斥锁已经被锁定。

不会调用 m.lock(),因为假设互斥锁已经被锁定。

在析构函数中调用 m.unlock(),释放互斥锁。

std::mutex mtx;
mtx.lock(); // 显式锁定互斥锁
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock); // 假设互斥锁已经被锁定
// 在这里可以安全地访问共享资源

也就是锁已经在之前锁了,也就是lock_guard不会再去锁,只是帮我保存,出了作用域就析构解锁而已!

拷贝构造函数

lock_guard(const lock_guard&) = delete;

作用:禁止拷贝构造。互斥锁是不可拷贝的,因此 lock_guard 也不支持拷贝构造。如果允许拷贝,可能会导致多个 lock_guard 对象同时管理同一个互斥锁,从而引发未定义行为。如果尝试使用拷贝构造函数,编译器会报错。

5. unique_lock

unique_lock 也是 C++11 提供的用于支持 RAII 方式管理互斥锁资源的类,相比 lock_guard,它的功能支持更丰富复杂。这是 unique_lock 的官方文档。 

unique_lock 首先在构造的时候传不同的 tag,用以支持在构造的时候不同的方式处理锁对象。

描述中文注释
(no tag)Lock on construction by calling member lock.构造时通过调用成员函数 lock 来锁定。
try_to_lockAttempt to lock on construction by calling member try_lock构造时尝试通过调用成员函数 try_lock 来锁定
defer_lockDo not lock on construction (and assume it is not already locked by thread)构造时不锁定(假设线程未锁定)
adopt_lockAdopt current lock (assume it is already locked by thread).采用当前锁(假设线程已经锁定)。
// unique_lock constructor example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock, std::unique_lock// std::adopt_lock, std::defer_lock
std::mutex foo,bar;void task_a () {std::lock (foo,bar);         // simultaneous lock (prevents deadlock)std::unique_lock<std::mutex> lck1 (foo,std::adopt_lock);std::unique_lock<std::mutex> lck2 (bar,std::adopt_lock);std::cout << "task a\n";// (unlocked automatically on destruction of lck1 and lck2)
}void task_b () {// foo.lock(); bar.lock(); // replaced by:std::unique_lock<std::mutex> lck1, lck2;lck1 = std::unique_lock<std::mutex>(bar,std::defer_lock);lck2 = std::unique_lock<std::mutex>(foo,std::defer_lock);std::lock (lck1,lck2);       // simultaneous lock (prevents deadlock)std::cout << "task b\n";// (unlocked automatically on destruction of lck1 and lck2)
}int main ()
{std::thread th1 (task_a);std::thread th2 (task_b);th1.join();th2.join();return 0;
}

这里补充一点: 

std::lock 是 C++ 标准库中用于同时锁定多个互斥锁的函数模板,它的主要作用是防止死锁。

在多线程程序中,如果多个线程需要同时锁定多个互斥锁,可能会因为锁的获取顺序不一致而导致死锁。std::lock 可以同时锁定多个互斥锁,确保它们被以一种安全的方式获取,从而避免死锁。

std::lock 是一个函数模板,定义在 <mutex> 头文件中,其语法如下:

template <class... Mutexes>
void lock(Mutexes&... m);
  • 参数Mutexes&... m 是一个参数包,表示可以传递多个互斥锁对象。

  • 返回值std::lock 不返回任何值,它直接锁定所有传入的互斥锁。

std::lock 的主要逻辑是:

  1. 尝试锁定所有互斥锁std::lock 会尝试以一种安全的顺序锁定所有传入的互斥锁。

  2. 避免死锁:它通过一种特殊的算法(通常是尝试锁的顺序排序)来确保不会因为锁的获取顺序不一致而导致死锁。

  3. 原子操作std::lock 的锁定过程是原子的,即在锁定所有互斥锁之前,不会释放任何已经锁定的互斥锁。

std::lock 通常用于以下场景:

  • 多锁同步:当一个线程需要同时锁定多个互斥锁时,使用 std::lock 可以避免死锁。

  • 线程安全的资源管理:在需要同时访问多个受保护的资源时,std::lock 确保这些资源的访问是线程安全的。

我们也就大概能懂上面这个代码了:

如果将两个锁对象给了unique_lock,那么unique_lock如果不带tag的话,直接锁的话,没有其他人锁的话就锁了,有就阻塞!可能就达不到我们想要的效果!我们想要先锁着了,然后交给unique_lock来进行管理!

也可以是task_b下的用法:先创建两个unique_lock,然后将两个锁交给unique_lock,但是是以推迟defer的方式:先不锁,暂且交给unique_lock管理,析构就释放就可以了,然后再对两个锁直接lock!


unique_lock 还可以在构造的时候传时间段和时间点,用来管理 timed_mutex 系统,构造时调用 try_lock_fortry_lock_until

unique_lock 不支持拷贝和赋值,支持移动构造和移动赋值。

unique_lock 还显示提供了 lock/try_lock/unlock 等系列的接口,这就更好控制了,和mutex类似的!

unique_lock 还可以通过 operator bool 去检查是否锁了锁对象。和owns_lock函数调用是一样的效果!

6. lock 和 try_lock

  • lock 是一个函数模板,可以支持对多个锁对象同时锁定。如果其中一个锁对象没有锁住,lock 函数会把已经锁定的对象解锁,然后进入阻塞,直到锁定所有的对象。(具体上面已经说过了)

  • try_lock 也是一个函数模板,尝试对多个锁对象进行同时锁定。如果全部锁对象都锁定了,返回 -1;如果某个锁对象尝试锁定失败,则把已经锁定成功的锁对象解锁,并返回这个对象的下标(第一个参数对象,下标从 0  开始算)。

template <class Mutex1, class Mutex2, class... Mutexes>
void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);template <class Mutex1, class Mutex2, class... Mutexes>
int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

 

// std::lock example
#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lockstd::mutex foo, bar;void task_a() {std::this_thread::sleep_for(std::chrono::seconds(1));foo.lock();bar.lock(); // replaced by://std::lock(foo, bar);std::cout << "task a\n";foo.unlock();bar.unlock();
}void task_b() {std::this_thread::sleep_for(std::chrono::seconds(1));bar.lock(); foo.lock(); // replaced by://std::lock(bar, foo);std::cout << "task b\n";bar.unlock();foo.unlock();
}int main()
{std::thread th1(task_a);std::thread th2(task_b);th1.join();th2.join();return 0;
}

 一个线程走task_a,另一个线程走task_b,task_a先去锁foo,再去锁bar,task_b相反 --- 这时候,在某些场景下就会有问题:

如果线程同时进来对应的额task,task_a先锁foo,同时task_b锁了bar,这时候就会导致死锁!双方会一直相互阻塞!

但是我们使用lock来进行同时锁:

std::lock 可以同时锁定多个互斥锁,确保它们被以一种安全的顺序获取,从而避免死锁。

  • task_atask_b 中,我们使用 std::lock(foo, bar)std::lock(bar, foo) 来同时锁定两个互斥锁。

  • std::lock 会尝试以一种安全的顺序锁定所有互斥锁,如果某个锁已经被其他线程持有,它会阻塞当前线程,直到所有锁都被成功锁定。

7. std::call_once

功能:在多线程执行时,确保某个函数(Fn)只被第一个线程执行一次,其他线程不会再次执行该函数。

函数模板声明

template <class Fn, class... Args>
void call_once (once_flag& flag, Fn&& fn, Args&&... args);

参数

  • flag:一个 std::once_flag 对象,用于标记函数是否已经被执行过。

  • fn:要执行的函数或可调用对象。

  • args:传递给 fn 的参数,支持可变参数。

行为

  • 如果 flag 表示函数尚未执行,则 call_once 会调用 fn,并将 args 转发给它。

  • 如果 flag 表示函数已经执行过,则 call_once 不会再次调用 fn

  • call_once 确保即使多个线程同时调用它,fn 也只会被调用一次。

call_once example
#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::milliseconds
#include <mutex>          // std::call_once, std::once_flagint winner;
void set_winner(int x) { winner = x; }
std::once_flag winner_flag;void wait_1000ms(int id) {// count to 1000, waiting 1ms between increments:for (int i = 0; i < 100; ++i)std::this_thread::sleep_for(std::chrono::milliseconds(1));// claim to be the winner (only the first such call is executed):std::call_once(winner_flag, set_winner, id);
}

这段代码展示了如何使用 std::call_oncestd::once_flag 来确保在多线程环境中某个操作只被第一个线程执行一次。具体来说:

  1. 线程任务:每个线程都会执行 wait_1000ms 函数,该函数模拟了一个耗时操作(通过循环调用 std::this_thread::sleep_for 模拟等待 1000 毫秒)。

  2. 竞争条件:所有线程在完成等待后,都会尝试调用 set_winner 函数来设置全局变量 winner 的值为当前线程的 ID。

  3. std::call_once 的作用:通过 std::call_oncestd::once_flag,确保只有第一个完成等待的线程能够成功调用 set_winner,其他线程的调用会被忽略。这样可以避免多个线程同时修改全局变量 winner,从而避免竞争条件。

  4. 结果:最终,winner 的值会被设置为第一个完成等待的线程的 ID,而其他线程的尝试不会改变这个值。

简而言之,这段代码通过 std::call_once 确保在多个线程中只有第一个完成任务的线程能够设置全局变量 winner,从而避免了多线程环境下的竞争条件。

http://www.xdnf.cn/news/1178371.html

相关文章:

  • 【YOLO系列】YOLOv4详解:模型结构、损失函数、训练方法及代码实现
  • PA333H-2K功率计:光伏行业高压测试“刚需”
  • 智慧驾驶疲劳检测算法的实时性优化
  • ARM 学习笔记(四)
  • 嵌入式软件--stm32 DAY 9 定时器
  • Springmvc的自动解管理
  • 一文说清楚Hive中常用的聚合函数[collect_list]
  • 一文读懂 HTTPS:证书体系与加密流程
  • Percona pt-archiver 出现长事务
  • GISBox实操指南:如何将IFC文件高效转换为3DTiles格式‌‌
  • 【MAC电脑系统变量管理】
  • 基于Zig语言,opencv相关的c++程序静态交叉编译
  • 微服务-springcloud-springboot-Skywalking详解(下载安装)
  • 设置后轻松将 iPhone 转移到 iPhone
  • 基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
  • Socket编程入门:从IP到端口全解析
  • C语言(长期更新)第5讲:数组练习(三)
  • Apache 消息队列分布式架构与原理
  • 开发避坑短篇(5):vue el-date-picker 设置默认开始结束时间
  • LLM层归一化:γβ与均值方差的协同奥秘
  • 力扣面试150题--在排序数组中查找元素的第一个和最后一个位置
  • 5.7 input子系统
  • 「Linux命令基础」查看用户和用户组状态
  • Silly Tavern 教程②:首次启动与基础设置
  • 文件管理困境如何破?ZFile+cpolar打造随身云盘新体验
  • Apache Flink 实时流处理性能优化实践指南
  • TRUMPF TruConvert DC 1008 – 1010 TruConvert System Control 逆变器
  • 货车手机远程启动的扩展功能有哪些
  • 从零用java实现 小红书 springboot vue uniapp(15) 集成minio存储 支持本地和minio切换
  • 如何在 Ubuntu 24.04 服务器或桌面版上安装和使用 gedit