记录C++11多线程开发的一些特性的使用。本文以使用源码的形式呈现,各个特性的使用在每个相关函数中说明。
常见特性使用
#pragma once
#include<iostream>
#include<thread>
#include<mutex>
#include<queue>
#include<future>
#include<vector>
#include<atomic>
//<<1<基础的lock_guard/unique_lock使用>>
void baseLock() {
std::mutex m1;
std::mutex m2;
//同时锁两个锁,若检测到冲突会释放,不会引发死锁
std::lock(m1, m2);
//<lock_guard>
//使用lock_guard自动释放锁。这里使用std::adopt_lock可以避免初始化时再调用m1/m2的lock函数
std::lock_guard<std::mutex> sbguard1(m1, std::adopt_lock);
std::lock_guard<std::mutex> sbguard2(m2, std::adopt_lock);
std::chrono::milliseconds dura(2000); // 时间:2000ms
std::this_thread::sleep_for(dura);
//<unique_guard>
//1.unique_lock取代lock_guard,可完全取代,更灵活,但耗用内存更高
//支持更多加锁方式
// 1.std::try_to_lock //使用该参数时mutex一定不能已经执行了lock函数
std::unique_lock<std::mutex> sbguard3(m1, std::try_to_lock);
if (sbguard3.owns_lock()) {} //判断是否拿到锁
// 2.std::adopt_lock 跟上面lock_guard一样
//3. std::defer_lock 同上,不能提前lock。初始化一个没有加锁的unique_lock.后面手动加锁,不需要手动解锁
// 成员函数:
//lock(), unlock(),
//try_lock()[加锁成功返回true]
//release()[返回管理的mutex指针,并解除两者的绑定关系.若未解锁,需手动调用mutex解锁]
//4.所有权转移
// unique_lock可以执行移动构造函数,转移mutex所有权
std::unique_lock<std::mutex> sbguard4(std::move(sbguard3));
//还可以通过函数返回
std::mutex m;
auto rtn_unique_lock = [&m]() {
std::unique_lock<std::mutex> sbguard(m);
return sbguard;
};
std::unique_lock<std::mutex> sbguard5 = rtn_unique_lock();
}
//<<2.<call_once函数实现单例模式>>>
void create_once() {}
void call_once() {
//锁住代码的多少叫做锁的粒度
//<std::call_once>
//C++11引入,保证管理的函数只被调用一次,具备互斥能力,效率上优于互斥量;
//std::call_once需要与一个标记std::once_flag结合使用.其实际为一个结构体,call_once调用完成后
//once_flag就被设置成已被调用状态。
//可用于改造单例模式
//example
// static void create_once() {}
void create_once();
std::once_flag g_flag;
std::call_once(g_flag, create_once);//g_flag类似一把锁。两个线程同时执行到这,其中一个线程需要等到另一个执行完
//create_once,再决定是否执行。create_once执行完之后g_flag被设置为已执行,那么第二个线程则不执行create_once
}
//<<3.<condition_variable>实现一个生产者消费者代码>>
std::queue<int> MsgRecvQueue;
std::condition_variable my_cond;
std::mutex m3;
void inMsgRecvQueue() {
std::cout << "creater.." << std::endl;
for (int i = 0; i < 10000; i++) {
std::unique_lock<std::mutex> sbGuard(m3);
MsgRecvQueue.push(i);
std::cout << "插入:" << i << std::endl;
my_cond.notify_one();
}
}
void outMsgRecvQueue() {
int nTimes = 0;
while (true) {
std::unique_lock<std::mutex> sbGuard(m3);
//my_cond.wait(sbGuard, [] { //第二个参数为false,则堵塞并释放锁
//if (!MsgRecvQueue.empty())
//return true;
//return false;
//});
//改造成以下形式,避免虚假唤醒
while (MsgRecvQueue.empty()) {
std::cout << "wait.." << std::endl;
my_cond.wait(sbGuard); // 即使被唤醒,拿到锁后仍要检查条件是都满足
}
int val = MsgRecvQueue.front();
MsgRecvQueue.pop();
++nTimes;
//走到这时一定是拿到了锁的
std::cout << "线程id:" << std::this_thread::get_id() << ",取出:" << val << std::endl;
}
}
void ConditionVariable() {
void inMsgRecvQueue();
void outMsgRecvQueue();
std::thread t1(outMsgRecvQueue);
inMsgRecvQueue();
t1.join();
}
//<<4<std::async执行异步任务>>>
int mythread() {
std::cout << "mythread srtart, thread id=" << std::this_thread::get_id() << std::endl;
std::chrono::microseconds dura(5000);
std::this_thread::sleep_for(dura);
std::cout << "mythread end, thread id=" << std::this_thread::get_id() << std::endl;
return 5;
}
void async_main() {
//<std::async>
//std::async是个函数模板,用来启动一个异步任务,启动后返回一个std::future对象
//std::future对象含线程入口函数返回的结果。
//std::future是一种访问异步操作结果的方法。通过future的get()函数等待程序运行结束返回运行结果
int mythread();
//可以加入几个额外参数实现一些特殊功能:
//1.std::launch::deferred ->延迟到调用wait和get函数时启动线程入口函数,如果不调用这两个函数,函数不会执行:
//std::async(std::launch::deferred, mythread); //【!这个线程实际根本没创建,函数就是在主线程里执行的!】
//2.std::launch::async,立即创建新线程
//默认参数(缺省参数时):std::launch::async|std::launch::deferred
std::future<int> result = std::async(mythread); //future对象和这个线程完成绑定.程序不会卡在这里
//如果用|传入两个参数,那么系统将根据资源情况自行选择是否创建新线程(也是默认参数)
// std::future<int> result = std::async(std::launch::deferred| std::launch::async, mythread);
//std::future<int> result = std::async(&A::mythread, &a, tmpar); //调用类成员函数并传参的方法
std::cout << "continue..." << std::endl;
int res = result.get(); // 卡在这里等待线程函数执行完。get只能调用一次
//result.wait() //这个只等待线程结束,并不会返回值
std::cout << res << std::endl;
std::cout << "线程id:" << std::this_thread::get_id() << ",取出:" << std::endl;
//如果不调用get、wait函数,那么线程会在主线程会在最后(return之前)等待线程执行完毕
//std::async支持std::launch::deferred【延迟调用】和std::launch::async【强制创建一个线程】
//std::thread(),如果系统资源紧张,创建线程可能失败导致系统崩溃。如果线程返回值,不容易获取
//std::async(),一般不叫创建线程,而是创建异步任务,因为有时std::async()并不创建新线程。容易获得线程函数返回值。
//默认方式调用时,如果系统资源紧张,那么就不会创建新线程。异步任务执行在调用future对象的get函数的线程。
//通过一下方法观察异步任务是否已经创建
//std::future_status,枚举值,有三个状态,ready,timeout,deferred
std::future<int> result2 = std::async(mythread);
std::future_status status = result2.wait_for(std::chrono::seconds(1));//等待0s即可判断状态
if (status == std::future_status::timeout) { //等待1s仍未返回结果
std::cout << "time out, the thread is not over" << std::endl;
}
if (status == std::future_status::ready) {
auto res = result2.get(); // 这是一定能获得结果的
}
if (status == std::future_status::deferred) {
//异步任务被延迟执行了
//如果async的第一个条件被设置成std::launch::deferred,未调用get/wait,则本条件成立
}
}
//<<5.package_task>>
//std::package_task类模板,打包任务。可以将各种可调用对象打包
void package_task() {
std::cout << "package_task()" << std::endl;
std::cout << "mythread srtart, thread id=" << std::this_thread::get_id() << std::endl;
std::packaged_task<int()> mypt(mythread);
// mypt(); // 可直接调用
//!!同一个packaged_task,只能被调用一次!包含被移动拷贝后!
//mypt()/mypt2()/mypt3()/std::thread t1(std::ref(mypt3));只能执行一个
//可以通过将任务通过packaged_task包装,放入一个vector容器
std::vector<std::packaged_task<int()>> mytasks;
std::packaged_task<int()> mypt2(std::move(mypt));
// mypt2();
mytasks.push_back(std::move(mypt2)); //这里也要使用移动语义
auto iter = mytasks.begin();
std::packaged_task<int()> mypt3 = std::move(*iter);
mytasks.erase(iter);
// mypt3();
//std::function也可以完成类似功能
std::thread t1(std::ref(mypt3)); // 1是mythread的参数
t1.join();
//package_task包装起来的可调用对象后也可以直接调用,相当于函数调用
//mypt(1);
//和packaged_task绑定,获取线程入口函数的执行结果
std::future<int> result = mypt3.get_future();
std::cout << result.get() << std::endl;
}
//<<6.std::promise使用>>
// std::promise也是个类模板。可以在某个线程给它赋值,然后在其他线程中,把它取出来
// 实现两个线程间的数据传递
void mythread1(std::promise<int>& temp, int calc) {
//一系列运算
calc++;
//...
std::chrono::milliseconds dura(5000);
std::this_thread::sleep_for(dura);
//计算出结果了
int res = calc;
temp.set_value(res); // 保存结果到std::promise<int>对象中
}
void mythread2(std::future<int>& temp) {
auto res = temp.get();
std::cout << "mythread2 res:" << res << std::endl;
}
void promise_main() {
std::cout << "promise_main()" << std::endl;
std::promise<int> myprom;
//线程1执行运算,将结果存进std::promise对象
//thread参数中的引用要使用std::ref
std::thread t1(mythread1, std::ref(myprom), 10);
t1.join();
//先将一个std::future对象和上面的std::promise绑定
std::future<int> fu = myprom.get_future(); //和promise绑定
//然后mythread2就可以从std::future对象中获取mythread1的运算结果
std::thread t2(mythread2, std::ref(fu));
t2.join();
}
// 总结:std::future获取线程的返回结果,或者和promise对象绑定获取结果
//<<7.std::future其他函数>>
int mythread3() {
std::cout << "mythread srtart, thread id=" << std::this_thread::get_id() << std::endl;
std::chrono::microseconds dura(5000);
std::this_thread::sleep_for(dura);
std::cout << "mythread end, thread id=" << std::this_thread::get_id() << std::endl;
return 5;
}
//<8.shared_future>
void future_main() {
std::cout << "future_main()"<< std::endl;
std::cout << "thread id=" << std::this_thread::get_id() << std::endl;
std::future<int> result = std::async(mythread3); // 程序不会卡在这
// std::cout << result.get() << std::endl; //卡在这里等待线程执行完毕;get只能执行一次
//补充:为什么std::future的get函数只能执行一次?因此get()返回是一个移动语义。
//std::shared_future的get()返回就是一个拷贝语义,可以多次get
if (result.valid()) {//result是否含有效值
std::shared_future<int> result_s(std::move(result)); //要使用移动语义初始化
// std::shared_future<int> result_s(result.share()); //或者这种形式
for (int i = 0; i < 5; ++i) {
std::cout << result_s.get() << std::endl; // 可以多次get
}
}
}
//<9.atomic>
void atomic_main() {
//原子变量的哪些操作时原子操作
std::atomic<int> count(0); // 原子初始化
count++;//原子自增
count += 1;//原子自增
count = count + 1;//不是原子操作
std::cout << count << std::endl; // 不是原子操作。count在往缓冲区写数据的过程中可能被其他线程改变当前值
// 因此显示的可能是曾经值
//原子类型不允许拷贝构造
std::atomic<int> a;
//std::atomic<int> b = a; //编译错误
std::atomic<int> b(a.load());//a.load(),以原子方式读
//a.store(x) // 以原子方式存
}
//<9.各种互斥量>
std::mutex m;
std::recursive_mutex rm; //递归独占互斥量
std::timed_mutex tm; //带超时功能的递归独占互斥量
//含有 try_lock_for()//一段时间内没拿到锁流程就继续往下走
// try_lock_until() //尝试加锁到某个时间点,如果到了时间还没拿到则流程继续往下走
void text1() {
m.lock();
//执行一些操作
m.unlock();
}
void text2() {
m.lock();
//执行一些操作
text1(); // 如果text2调用text1,那么这里使用mutex会崩溃
// std::recursive_mutex则可以
m.unlock();
}
void recursive_mutex() { // 递归的独占互斥量
//一般的mutex只能加锁/解锁一次
text2();
std::chrono::microseconds timeout(100);
if (tm.try_lock_for(timeout)) {
//在这100ms里拿到了锁
// do something
tm.unlock();
}
else {
//没拿到锁
}
if (tm.try_lock_until(std::chrono::steady_clock::now() + timeout)) {
//在100ms后的时刻拿到了锁
// do something
tm.unlock();
}
else {
//没拿到锁
}
}
一些知识点补充
1.条件变量使用
谨防虚假唤醒
在正常情况下,wait类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因此操作系统的原因,wait类型在不满足条件时,它也会返回,这就导致了虚假唤醒。因此,我们一般都是使用带有谓词参数的wait函数,因为这种(xxx, Predicate pred )类型的函数等价于:
while (!pred()) //while循环,解决了虚假唤醒的问题
{
wait(lock);
}
原因说明如下:
假设系统不存在虚假唤醒的时,代码形式如下:
if (不满足xxx条件)
{
//没有虚假唤醒,wait函数可以一直等待,直到被唤醒或者超时,没有问题。
//但实际中却存在虚假唤醒,导致假设不成立,wait不会继续等待,跳出if语句,
//提前执行其他代码,流程异常
wait();
}
//其他代码
···
正确的使用方式,使用while语句解决:
while (!(xxx条件) )
{
//虚假唤醒发生,由于while循环,再次检查条件是否满足,
//否则继续等待,解决虚假唤醒
wait();
}
//其他代码
···
使用条件变量,解决生产者-消费者问题
- 生产者-消费者问题,也称有限缓冲问题,是一个多进程/线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程/线程——即所谓的“生产者”和“消费者”,在实际运行时会发生的问题。
生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。- 要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。
同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
生产者-消费者代码如下:
std::mutex g_cvMutex;
std::condition_variable g_cv;
//缓存区
std::deque<int> g_data_deque;
//缓存区最大数目
const int MAX_NUM = 30;
//数据
int g_next_index = 0;
//生产者,消费者线程个数
const int PRODUCER_THREAD_NUM = 3;
const int CONSUMER_THREAD_NUM = 3;
void producer_thread(int thread_id)
{
while (true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
//加锁
std::unique_lock <std::mutex> lk(g_cvMutex);
//当队列未满时,继续添加数据
g_cv.wait(lk, [](){ return g_data_deque.size() <= MAX_NUM; });
g_next_index++;
g_data_deque.push_back(g_next_index);
std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
std::cout << " queue size: " << g_data_deque.size() << std::endl;
//唤醒其他线程
g_cv.notify_all();
//自动释放锁
}
}
void consumer_thread(int thread_id)
{
while (true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(550));
//加锁
std::unique_lock <std::mutex> lk(g_cvMutex);
//检测条件是否达成
g_cv.wait( lk, []{ return !g_data_deque.empty(); });
//互斥操作,消息数据
int data = g_data_deque.front();
g_data_deque.pop_front();
std::cout << "\tconsumer_thread: " << thread_id << " consumer data: ";
std::cout << data << " deque size: " << g_data_deque.size() << std::endl;
//唤醒其他线程
g_cv.notify_all();
//自动释放锁
}
}
详细请查阅:条件变量详细使用细则