C++11多线程编程常用特性使用案例

记录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();
        //自动释放锁
    }
}

详细请查阅:条件变量详细使用细则

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋风遗梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值