并发:表示两个或者多个任务同时发生。
并行:只得就是在同一时刻,有两个或多个任务(进程),代码在处理器上执行,必须是多核CPU
并发并不是并行,并发关乎结构,并行关乎执行
C++基于多线程模型的应用设计就是一种典型的并发程序设计
Thread实现线程
main相当于主线程,如果保持子线程一直处于执行状态,必须让主线程保持执行,千万不要主线程执行完毕。
1 |
|
一个书写良好的程序: 主线程等待子线程执行完毕,自己才能最终退出
detach
detach是一种让子线程和主线程分离的方法,主线程执行主线程的,子线程执行子线程的,新创建的子线程被C++ 运行时库接管了,当这个线程执行完毕后,运行时库负责清理该线程的相关资源,相当于linux里面的守护线程
对于同一个线程一但用detach,就不能用join,否则导致程序异常
joinable
joinable判断是否可以用join或detach
1 | std::thread myobj(myprint); |
用类来创建线程
1 | class Ta { |
ta对象是一个复制,用myobj.detach 没有影响,但是如果对象里面有引用或指针就会产生问题,如构造函数
Ta(int& i)
主线detach后会销毁 变量
lamda表达式来创建线程
1 | auto mylamda = [] |
所以如果真要用detach这种方式创建线程,记住不要往线程中传递引用、指针之类的参数
如果用detach创建线程,会有些陷阱
- 如果传递int这种简单类型参数,建议都使用值传递
- 如果传递类对象作为参数,则避免隐式类型转换(例如把一个char*转成string,把一个int转成类A对象),全部都在创建线程这一行就构建出临时对象来,然后线程入口函数的形参位置使用引用来作为形参
1 |
|
执行结果,发现哪怕用detach执行,程序没有异常,输出拷贝构造函数,且拷贝构造函数线程id和主线程一致,说明拷贝构造函数发生在主线程执行的过程,使用临时对象作为实参,就可以确保主线程入口的行参数在主线程执行完毕前就已经创建完毕,可以安全使用。
传递类对象
为了数据安全,往线程入口函数传递类类型对象作为参数的时候,不管接收者(形参)是否用引用接收,都一概采用复制对象的方式来进行参数的传递。C++编译器加入了临时对象不能作为非const引用的语义限制,但是如果加上了const要修改类对象成员(一般没必要这么做),就非常不方面,除非每一个类成员变量都加上mutable 。这里可以用一个函数模板 std::ref
1 |
|
用成员函数作为线程入口函数
1 | void thread_work(int num) |
互斥量加锁处理共享数据竞争
锁定的代码段越少,执行的效率越高,因为锁住的代码少,执行得快,其他等待锁的线程等待的时间就短
1 |
|
std::lock_guardstd::mutex的工作原理,其实它的工作原理很简单,这样理解:在lock_guard类模板的构造函数里,调用了mutex的lock成员函数,而在析构函数里,调用了mutex的unlock成员函数,使用了locak_guard 就不能重复使用 mutext.lock
死锁问题:
多个进程或线程竞争同一不可抢占的资源时,如果每个进程都持有某种资源并等待获取其他资源,就会导致死锁,为了避免死锁,采用有序资源分配法。
死锁演示代码: 创建一个新的互斥两my_mutex2 ,第一个线程 按my_mutex my_mutex2 的顺序加锁 释放锁
第二个线程 按 my_mutex2 my_mutex 的顺序加锁 释放锁
1 |
|
可以看到某个时刻,程序锁住了,执行不下去了
std::lock函数模版
std::lock函数模板能一次锁住两个或者两个以上的互斥量(互斥量数量是2个到多个,不可以是1个),它不存在多个线程中因为锁的顺序问题导致死锁的风险。
std::lock(my_mutex,my_mutex2);
std::lock_guard参数 std::adopt_lock:
相当于 互斥量已经lock过了,无需要再次lock ,只要在析构函数中执行unlock就可以了
unique_lock
unique_lock和lock_guard一样,都是用来对mutex(互斥量)进行加锁和解锁管理,但是,lock_guard不太灵活:构造lock_guard对象的时候lock互斥量,析构lock_guard对象的时候unlock互斥量。相比之下,unique_lock的灵活性就要好很多,当然,灵活性高的代价是执行效率差一点,内存占用的也稍微多一点
1 | my_mutex.lock(); |
unique_lock参数std::try_to_lock :
开发者不能自己先去lock这个mutex
1 |
|
unique_lock 参数std::defer_lock
开发者不能自己先去lock这个mutex ,初始化一个没有加锁的mutex
处理一些非共享数据 sbgard1.unlock()
1 | std::unique_lock<std::mutex> sbgard1(my_mutex, std::defer_lock); |
release返回它所管理的mutex对象指针,并释放所有权。
一旦解除该unique_lock和所管理的mutex的关联关系,如果原来mutex对象处于加锁状态,则程序员有责任负责解锁。
1 | std::unique_lock<std::mutex> sbgard1(my_mutex); |
unique_lock会在离开作用域的时候检查关联的mutex是否lock,如果lock了,unique_lock会帮助程序员unlock
unique_lock所有权的传递
std::unique_lock<std::mutex> sbgard2(std::move(sbgard1));
第二种用函数返回return
1 |
|
一个多线线程单例模式案例
1 |
|
condition_variable、wait、notify_one与notify_all
线程A中等待一个条件满足(如等待消息队列中有要处理的数据),另外还有个线程B(专门往消息队列中扔数据),当条件满足时(消息队列中有数据时),线程B通知线程A,那么线程A就会从等待这个条件的地方往下继续执行。
1 |
|
std::async和std::future的用法
std::async是一个函数模板,通常的说法是用来启动一个异步任务,启动起来这个异步任务后,它会返回一个std::future对象(std::future是一个类模板)。
1 |
|
std::async 额外参数
std::launch::deferred 不创建新线程且延迟调用
1 | std::future<int> result = std::async(std::launch::deferred,&A::mythread, &a, temppar); |
调用get 或wait后发现mythread 被执行了,但是子线程和主线程id相同,说明并没有创建新的线程,而是在主线程调用的mythread
std::launch::async
1 | std::future<int> result = std::async(std::launch::async,&A::mythread, &a, temppar); |
调用async后线程会创建并立即执行线程
1 | std::future<int> result = std::async(std::launch::async | std::launch::deferred,&A::mythread, &a, temppar); |
“|“ 表示创建新线程并执行或者没有创建新新线程且延迟调用,系统根据一定因素去评估,自行选择(相当于不用任何参数)
std::async 是异步任务可创建线程也可不创建线程,和std::thread不同 ,在资源紧张时候可以用async代替 thread
std::packaged_task
这是一个模版类,模版参数是各种可调用对象,通过packaged_task 把各种可调用对象包装起来,方便将来作为线程入口函数调用
1 | cout << "main threadid = " << std::this_thread::get_id() << endl; |
1 | int main() |
这个 案例好复杂,学c++ 后,一直没理解为什么这些大神搞这么多的模版函数,其实工作还是用最简单的std:: thread 就可以搞定。
std::promise
这个类模版,能够在某个线程中为其赋值,然后就可以在其他线程中,把这个值取出来
1 | void mythread(std::promise<int> & tmpp,int calc) |
总结下来就是通过promis保存一个值,在将来某个时刻把一个future绑定到这个promise上
future其他成员函数
1 |
|
std::shared_future
shared_future的get函数把数据进行复制而不是转移
1 | std::shared_future<int> result_s(std::move(result)); |
原子操作
用互斥量mutex 处理共享变量,效率很低,互斥量更适合对一段代码加锁,对单个共享变量我们用std::atomic
atomic 用法非常简单
1 | std::atomic<int> g_mycout = 0; |
recursive_mutex递归的独占互斥量
1 | std::recursive_mutex my_mutext; |
std::timed_mutex是带超时功能的独占互斥量
是try_lock_for
一个是try_lock_until
1 | std::chrono::milliseconds timeout(100); |
std::recursive_timed_mutex是带超时功能的递归的独占互斥量
参考文献:《C++新经典》
- 本文作者: 东方觉主
- 本文链接: http://www.charon193.com/2024/05/20/cpulsthread/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!