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

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

并发支持库

C++11扩展 --- 并发支持库(上)https://blog.csdn.net/Small_entreprene/article/details/149334360?sharetype=blogdetail&sharerId=149334360&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link针对上一篇,我们下面继续学习C++11提供的并发支持库的相关内容!

3. <mutex> 

互斥锁是很重要的一个东西,首先我们来看一段程序:

#include <iostream>
#include <thread>
#include <mutex>std::mutex mtx;
int x = 0;void Print(int n)
{for (int i = 0; i < n; i++){mtx.lock();// t1 t2++x;mtx.unlock();}
}int main()
{std::thread t1(Print, 10000000);std::thread t2(Print, 20000000);t1.join();t2.join();std::cout << x << std::endl;return 0;
}

理论来说是应该为30000000的,但是输出的少了这么多!而且每次运行的还不一样!!!

提供的代码是一个 C++ 程序,其中包含两个线程 t1t2,它们都调用了 Print 函数。每个线程都会对全局变量 x 进行增加操作。然而,这段代码存在下面问题:

Print 函数中的 ++x 操作不是原子操作,这意味着在多线程环境下,这个操作可能会被中断,从而导致数据不一致。代码会被编译成指令:

是将x先从内存中取到寄存器,然后再CPU进行加法运算,然后在从寄存器返回给内存的!这不是一句语句,不是原子的!!!因为CPU一般不直接访问内存的,因为CPU太快了!!!内存有点慢! 


下面的链接是mutex的文档:

<mutex> - C++ Reference

  • Mutex 类型:用于保护临界区代码的互斥锁类型,包括 mutexrecursive_mutextimed_mutexrecursive_timed_mutex

  • Lock 类型:管理互斥锁的对象,包括 lock_guardunique_lock

  • 其他类型:如 once_flagadopt_lock_tdefer_lock_ttry_to_lock_t

  • 函数:如 try_locklockcall_once


Mutex 类

mutex:是封装的互斥锁类,用于保护临界区的共享数据。主要提供 lockunlock 两个接口函数。和我们所熟知的 pthread 一样,提供了相关的接口!只不过是以成员函数的方式提供的!

我们上面的样例代码就可以将去掉相关注释:

通过加锁和解锁,两个线程就可以变成串行行为了,但是效率是变低了,不过在安全性下,性能是第二位的!

但是我们可以想一想,是将加锁解锁放在for循环里面效率高,还是放在外面效率高呢? 

其实在外面是效率比较高的,放在外面就是一个线程将for循环执行完了,然后换另一个!,里面就是++一次就可能换另外一个线程++了,肯定是要经过好几次加锁解锁的,效率肯定是低的!!!

提供排他性(就是只有一个线程可以进入的,剩下线程进入阻塞状态)非递归所有权语义:

  1. 调用方线程从成功调用 locktry_lock 开始,到调用 unlock 为止占有互斥锁。

  2. 线程占有互斥锁时,其他线程如果试图要求互斥锁的所有权,那么就会阻塞(对于 lock 的调用),对于 try_lock 会返回 false

timed_mutex:与 mutex 完全类似,但额外提供 try_lock_fortry_lock_until 接口。这两个接口与 try_lock 类似,但不会马上返回,而是直接进入阻塞,直到时间条件到了或者解锁了才会唤醒试图获取锁资源的线程。

std::timed_mutex mtx;
void fireworks(int i)
{//std::cout << i;// waiting to get a lock: each thread prints "-" every 1s:while (!mtx.try_lock_for(std::chrono::milliseconds(1000))){std::cout << "-";}std::cout << i;// got a lock! - wait for 1s, then this thread prints "*"std::this_thread::sleep_for(std::chrono::milliseconds(5000));std::cout << "*\n";mtx.unlock();
}int main()
{//std::thread threads[2];vector<std::thread> threads(2);// 利用移动赋值的方式,将创建的临时对象(右值对象)移动赋值给创建好的空线程对象for (int i = 0; i < 2; ++i)threads[i] = std::thread(fireworks, i);for (auto& th : threads)th.join();return 0;
}
PS D:\2024C语言\C++加餐\c-additional-meal\C++11并发支持库加餐> g++ -o test Test.cpp
PS D:\2024C语言\C++加餐\c-additional-meal\C++11并发支持库加餐> ./test
0----*
1*

这段代码通过两个线程竞争一个定时互斥锁 std::timed_mutex 来控制对共享资源的访问。每个线程在尝试获取锁时,若未能在 1 秒内成功获取,会打印一个 "-" 表示等待。一旦获取锁,线程打印其编号(01),然后等待 5 秒,最后打印一个 "*" 并释放锁。由于锁是互斥的,两个线程会交替获取锁,最终输出类似于 "-0*1*" 的结果,具体顺序取决于线程的调度和锁的获取时机。 

recursive_mutex:与 mutex 完全类似,但提供排他性递归所有权语义:

  1. 调用方线程在从成功调用 locktry_lock 开始的时期里占有 recursive_mutex。此时期之内,线程可以进行对 locktry_lock 的附加调用。

  2. 所有权的时期在线程进行匹配次数的 unlock 调用时结束。

  3. 线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock)或收到 false 返回值(对于调用 try_lock)。

mutex mtx;
//recursive_mutex mtx;//需要使用递归锁!!!
void func(int n)
{mtx.lock();//...func(n-1);//这就是一个大坑,再次进入会阻塞,因为前一个没有unlock,就会造成死锁!mtx.unlock();
}

锁是不能被拷贝的,不然锁上的这些线程怎么办?!

如果互斥锁在,仍为任何线程所占有,这时候该mutex被销毁,或在占有互斥锁时线程终止,那么行为是未定义,结果是不知道了的,所以我们要保证这个锁或这些线程的生命周期是在的。


全局变量有很多的问题,比如说线程安全就是其中一个问题,还有我们定义全局变量是尽量避免全局变量含有链接属性的,全局变量通常应该避免定义在头文件(.h文件)中,因为头文件通常被包含(include)在多个源文件(.cpp文件)中。如果全局变量定义在头文件中,那么每个包含该头文件的源文件都会有该变量的一个定义,这会导致链接时的重复定义错误,是比较坑的!所以有些人就会按照下面的写法:

void Print(int n, int& rx, mutex& rmtx)
{rmtx.lock();for (int i = 0; i < n; i++){// t1 t2++rx;}rmtx.unlock();
}int main()
{int x = 0;mutex mtx;// 这里必须要用ref()传参,现成中拿到的才是x和mtx的引用,具体原因需要看下面thread源码中的分析// https://legacy.cplusplus.com/reference/functional/ref/?kw=refthread t1(Print, 1000000, ref(x), ref(mtx));thread t2(Print, 2000000, ref(x), ref(mtx));t1.join();t2.join();cout << x << endl;return 0;
}

使用传参!就是定义局部变量,将其传过去。但是局部变量不是独立的吗?

我们应该要平衡的看待这个问题,下面定义的x是在主线程中的,在主线程独立,然后将其传给了两个从线程,从线程还是共享的!只要访问共享资源,就会有线程安全的问题!当然了,还需要将锁传过去!

这时候就会有“ ref ”的概念!因为需要传引用的,不然后续+的就不是同一个 x 了,没有 ref,就会没有传引用的功能,不然我们直接传引用也是没有用的,这是和其底层的实现有关系的!

那我们为什么需要通过" ref "这种形式传参才可以?

我们进行传参,感官上是直接传给Print的,但是其实不是的,x 和 mtx 这两个参数是传递给 thread 这个构造函数的,thread 是C++的一个类,他不是凭空创建线程的,而是去调用其他各个系统的相关函数!在Windows下就是去调用自己对应的创建线程的函数,如果在Linux下就是去调用pthread_create(),无论是哪一个操作系统,创建线程的逻辑都是给一个函数指针,然后这个函数指针的特点是void*的返回值,然后传void*的参数。也就是传参的时候是需要先构建一个结构体,然后将这个结构体的指针作为参数的:

这就是内核里创建线程的相关概念了,然后让这个线程最终去执行函数指针指向的函数体了,然后再将这些参数解析出来,所以我们需要明白的第一个结论是:我们传递的参数并不是直接传递给Print的!

然后我们往thread的源码理解理解:

void* _Invoker_proc(void* ptr)
{_Tuple* tptr = (_Tuple*)ptr;//参数包解析Print(tptr[1], tptr[2], tptr[3]);//Print应用的其实是智能指针结构体里面的值,这些值就不是原本的x和mtx,而是拷贝了!
}

如果线程对象传参给可调用对象时,使用引用方式传参,实参位置需要加上 ref(obj) 的方式。主要原因是 thread 本质还是系统库提供的线程 API 的封装,thread 构造取到参数包以后,要调用创建线程的 API,还是需要将参数包打包成一个结构体传参过去。那么打包成结构体时,参数包对象就会拷贝给结构体对象,使用 ref 传参的参数,会让结构体中的对应参数成员类型推导为引用,这样才能实现引用传参。

std::ref 

我们可以看看ref的相关文档:


ref - C++ Reference

std::ref:用于构造一个 reference_wrapper 对象,以持有某个元素的引用。

template <class T> reference_wrapper<T> ref (T& elem) noexcept;template <class T> reference_wrapper<T> ref (reference_wrapper<T>& x) noexcept;template <class T> void ref (const T&&) = delete;

elem:一个左值引用,其引用被存储在对象中。

x:一个 reference_wrapper 对象,将被复制。

返回值:一个 reference_wrapper 对象,用于持有类型为 T 的元素的引用。

#include <iostream>     // std::cout
#include <functional>   // std::ref
int main () {int foo (10);auto bar = std::ref(foo);++bar;std::cout << foo << '\n';return 0;
}

输出:11

这段代码将输出 11,因为 foo 的初始值是 10,然后通过 bar(一个引用包装器)增加了 1。这里的 ++bar 直接修改了 foo 的值,因为 barfoo 的引用。


针对上面的传参问题,我们还用更加方便的写法:使用lambda的捕捉列表的语法!!!

int main()
{int x = 0;mutex mtx;// 将上面的代码改成使用lambda捕获外层的对象,也就可以不用传参数,间接解决了上面的问题auto Print = [&x, &mtx](size_t n) {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;
}

http://www.dtcms.com/a/294538.html

相关文章:

  • sqlsuger 子表获取主表中的一个字段的写法
  • 第一章:Go语言基础入门之Go语言安装与环境配置
  • 顺丰面试提到的一个算法题
  • OpenAI发布ChatGPT Agent,AI智能体迎来关键变革
  • Git原理及使用
  • android studio打包vue
  • Android Studio中调用USB摄像头
  • 广告业技术范式转移:当AI开始重构整个价值链
  • 硅基纪元:当人类成为文明演化的燃料——论AI终极形态下的存在论重构
  • 【Linux系统】基础IO(上)
  • Neo4j如何修改用户密码?
  • Codeforces Round 973 (Div. 2)
  • uniapp自定义圆形勾选框和全选框
  • 软件开发、项目开发基本步骤
  • MCU芯片AS32S601在卫星光纤放大器(EDFA)中的应用探索
  • NineData新增SQL Server到MySQL复制链路,高效助力异构数据库迁移
  • ubuntulinux快捷键
  • 「iOS」——KVC
  • ubuntu22.04 python升级并安装pip命令
  • 轻量化RTSP视频通路实践:采集即服务、播放即模块的工程解读
  • 第十讲:stack、queue、priority_queue以及deque
  • LeetCode 热题100:160.相交链表
  • [CH582M入门第十步]蓝牙从机
  • Acrobat JavaScript Console 调试控制台
  • 关于网络安全等级保护的那些事
  • 【MyBatis-Plus】核心开发指南:高效CRUD与进阶实践
  • 基于Kafka实现简单的延时队列
  • XiangJsonCraft:用JSON快速构建动态HTML页面的利器
  • 第十章 W55MH32 SNTP示例
  • LarkXR实时云渲染支持Quest客户端手势操作:免手柄直控云XR应用