系统的学习一下C++标准的多线程----同步并发操作

目录

等待事件或其他条件

用条件变量等待条件

使用future等待一次性事件

从后台任务中返回值

std::future

std::packaged_task

std::promise

std::async

等待自多个线程

有时间限制的等待


有时候你不只是需要保护数据,还需要在独立的线程上进行同步操作。例如,一个线程在能够完成其任务之前可能需要等待另外一个线程完成任务。一般来说,希望一个线程等待特定的事件的发生或者是一个条件变为true是常见的事情。虽然通过定期检查“任务完成”的标识或是在共享数据中存储类似的东西也能做到这一点,但是不甚理想。对于像这样的线程间同步操作的需求是如此常见,以至于C++标准提供了条件变量(condition)和期值(future)为形式的工具来处理它。

等待事件或其他条件

如果一个线程正等待着第二个线程完成一项任务他有几个选择:

第一:第一个线程一直检查共享数据(由互斥元保护)中的标识,并且让第二个线程在完成任务时设置该标识。

第二:选择使用std::this_thread::sleep_for()函数,参数时毫秒。让等待中的线程在检查之间休眠一会儿。

第三:同时也是首选,是使用C++标准库提供的工具来等待事件本身。等待由另外一个线程触发一个事件的最基本机制是条件变量。

从概念上说,条件变量与某些事件或其他条件相关,并且一个或多个线程可以等待该条件被满足。当某个线程已经确定条件得到满足,他就可以通知一个或者多个正在条件变量上进行等待的线程,以便唤醒他们并继续处理。

用条件变量等待条件

标准C++库提供了两个条件变量的实现std::condition_variable和std::condition_variable_any。这两个实现都在<condition_variable>库的头文件中两者都需要和互斥元一起工作,以便提供恰当的同步;前者仅限于和std::mutex一起工作,而后者则可以与符合成为类似互斥元的最低标准的任何东西一起工作,因此以_any为后缀。因为std::condition_variable_any更加普遍,所以会有大小、性能或者操作系统资源方面的形式的额外代价的可能,因为应该首选std::condition_variable,除非需要额外的灵活性。

当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。

示例代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace std;
std::condition_variable cdv; //全局条件变量
mutex mtx; //全局互斥锁
bool ready = false; //全局标志位
void func()
{
	std::unique_lock<std::mutex> lck(mtx);
	while (!ready)	//如果标识位不为true,则等待...
	{
		cdv.wait(lck);	// 当前线程被阻塞,当全局标识位变为true之后,线程被唤醒继续往下执行打印线程ID
	}
	cout << "thread ID = " << std::this_thread::get_id() << endl;	//打印当前线程的ID	
}
int main() 
{
	thread th[10];
	for (auto& t : th)
	{
		t = thread(func);
	}	
	thread t2([]() {
		std::unique_lock<std::mutex> lck(mtx);
		for (int i = 0; i < 20; ++i)
		{
			cout << "i = " << i << endl;
		}
		ready = true; //设置全局标识位为true
		cdv.notify_all();//唤醒所有线程
	});
	for (auto& t : th)
	{
		if (t.joinable())
		{
			t.join();
		}
	}
	if (t2.joinable())
	{
		t2.join();
	}
	return 0;
}

执行结果:

要是不同标识位也是可以的

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace std;
std::condition_variable cdv; //全局条件变量
mutex mtx; //全局互斥锁
void func()
{
	std::unique_lock<std::mutex> lck(mtx);
	cdv.wait(lck);	// 当前线程被阻塞,当全局标识位变为true之后,线程被唤醒继续往下执行打印线程ID
	cout << "thread ID = " << std::this_thread::get_id() << endl;	//打印当前线程的ID	
}
int main() 
{
	thread th[10];
	for (auto& t : th)
	{
		t = thread(func);
	}	
	thread t2([]() {
		std::unique_lock<std::mutex> lck(mtx);
		for (int i = 0; i < 20; ++i)
		{
			cout << "i = " << i << endl;
		}		
		cdv.notify_all();//唤醒所有线程
	});
	for (auto& t : th)
	{
		if (t.joinable())
		{
			t.join();
		}
	}
	if (t2.joinable())
	{
		t2.join();
	}
	return 0;
}

 结果跟上面的是一样的。

分享一个不错的博客

C++11 并发指南五(std::condition_variable 详解) - Haippy - 博客园 (cnblogs.com)

使用future等待一次性事件

如果等待线程只打算等待一次,那么当条件为true时它就不会再等待这个条件变量了,条件变量未必是同步机制的最佳选择。如果所等待的条件是一个特定数据块的可用性时,这尤其正确。在这个场景中使用期值(future)可能会更合适。

C++标准库使用future为这类一次性事件建模。如果一个线程需要等待特定的一次性事件,那么就会获取一个future来代表这一事件。然后,该线程可以周期性地在这个future上等待一小段时间以检查事件是否发生,而在检查间隙执行其他任务。另外,它还可以去做另外一个任务,直到其所需的事件已经发生才继续进行,随后就等待future变为就绪(ready)。future可能会有与之相关的数据,或者可能没有。一旦事件已经发生(即future已变为就绪),future就无法复位。

C++标准库中有两类future,是由<future>库的头文件中声明的两个类模板实现的:

唯一 future(unique future是,std::future<>)和共享future(shared futures,std::shared_future<>).这两个类模板是参照std::unique_ptr和std::shared_ptr 建立的。std::future的实例是仅有的一个指向其关联事件的实例,而多个std::shared_future的实例则可以指向同一个事件。对于后者而言,所有实例将同时变为就绪,并且它们都可以访问所有与该事件相关的数据。这些关联的数据就是这两种future成为模板的原因;像std::unique_ptr 和 std::shared_ptr 一样,模板参数就是关联数据的类型。

std::future<void>和std::shared_future<void>模板特化应该用于无关联数据的场合。

虽然future被用于线程间通信,但是future对象本身却并不提供同步访问。如果多线程需要访问同一个future对象,它们必须通过互斥元或其他同步机制来保护访问。

从后台任务中返回值

假设你有个长期运行的计算,预期最终将得到一个结果但是现在你还不需要这个值。你可以启动一个线程来执行计算,但这也意味着你必须注意将结果传回来,因为std::thread并没有提供直接的机制来这样做。这就是std::async函数模板。同样声明在<future>头文件中。

在不需要立即得到结果的时候,你可以使用std::async来启动一个异步任务std::async返回一个std::future对象,而不是给你一个std::thread对象让你在上面等待,std::future对象最终将持有函数的返回值。当你需要这个值时,只要在future上调用get(),线程就会阻塞直到future就绪,然后返回该值。

 std::async是一个函数模板,会启动一个异步任务,最终返回一个std::future对象。在之前我们都是通过thread去创建一个子线程,但是如果我们要得到这个子线程所返回的结果,那么可能就需要用全局变量或者引用的方法来得到结果,这样或多或少都会不太方便,那么async这个函数就可以将得到的结果保存在future中,然后通过future来获取想要得到的结果。async比起thread来说可以对线程的创建又有了更好的控制,比如可以延迟创建。下面先介绍一下std::future, std::packaged_task, std::promise。

std::future

       std::future是一个类模板,提供了一个访问异步操作的结果的机制。我们可以通过future_status去查询future的三种状态,分别是deferred(还未执行),ready(已经完成),timeout(执行超时),所以我们可以通过这个去查询异步操作的状态。future提供了一些函数比如get(),wait(),wait_for(),一般用get()来获取future所得到的结果,如果异步操作还没有结束,那么会在此等待异步操作的结束,并获取返回的结果。wait()只是在此等待异步操作的结束,并不能获得返回结果。wait_for()超时等待返回结果。

#include <iostream>
#include <thread>
#include <mutex>
#include <future>

using namespace std;
int func(int i)
{
	return 10 + i;	
}
int main() 
{
	std::future<int> fu = std::async(std::launch::async, func, 6); 
	cout << fu.get() << endl;
	return 0;
}

结果:

 

 

std::packaged_task

       std::packaged_task是一个类模板,顾名思义是用来打包的,将一个可调用对象封装起来,然后可以将其的返回值传给future。std::packaged_task<函数返回类型(参数类型)> 变量名(函数名)。下面展示一下std::packaged_task()的简单用法,也可以将函数换成lambda表达式。

std::packaged_task<>将一个future绑定到一个函数或者可调用对象上。当std::packaged_task<>对象被调用时,它就调用相关联的函数或者可调用对象,并且让future就绪,将返回值作为关联数据存储。这可以被用作线程池的构件,或者其他任务管理模式,例如在每个任务自己的线程上运行,或者一个特定的后台线程按顺序运行所有任务。

如果一个大型操作可以分为许多包含子任务,其中每一个都可以封装在一个std::packaged_task<>实例中,然后将该实例传给任务调度器或线程池。这样就抽象出了任务的详细信息,调度程序仅需要处理std::packaged_task<>实例,而非各个函数。

std::packaged_task<>类模板的模板参数为函数签名,比如void()表示无参数无返回值的函数,或者像int(string&,double*)表示接受一个对string的非const引用和double指针并返回int的函数。当你构造std::packaged_task实例的时候,你必须传入一个函数或者可调用对象,它可以接受指定的参数并且返回指定的类型。类型无需严格匹配,你可以用一个接受int并返回float的函数构造std::packaged_task<double(double)>,因为这些类型是可以隐式转换的。

指定的函数签名的返回类型确定了从get_future()成员函数返回std::future<>的类型。

#include <iostream>
#include <thread>
#include <mutex>
#include <future>

using namespace std;
int func(int i)
{
	return 10 + i;	
}
int main() 
{
	std::packaged_task<int(int)> pt(func);	//int(int)是因为返回值是int,函数参数也是int	//将函数打包起来
	std::future<int> fu = pt.get_future();	// 并将结果返回给future
	std::thread t(std::ref(pt), 1);
	std::cout << fu.get() << std::endl;
	std::cout << std::this_thread::get_id() << std::endl;
	t.join();
	return 0;
}

结果:

 

std::promise

 std::promise是一个类模板,它的作用是在不同的线程中实现数据的同步,与future结合使用,也间接实现了future在不同线程间的同步。

       promise还有一个函数是set_value_at_thread_exit()这个翻译一下就可以直到它的作用是当在这个线程执行结束的时候才会将future的状态设置为ready,而set_value()则直接将future的状态设置为ready。需要注意的是在使用的过程中不能多次set_value(),也不能多次get_future()和多次get(),因为一个promise对象只能和一个对象相关联,否则就会抛出异常。 

#include <iostream>
#include <future>
#include <thread>

int fun(int x, std::promise<int>& p) {
	x++;
	x *= 10;
	p.set_value(x);
	std::cout << std::this_thread::get_id() << std::endl;
	return x;
}
int main()
{
	std::promise<int> p;
	std::future<int> fu = p.get_future();        // 并将结果返回给future
	std::thread t(fun, 1, std::ref(p));
	std::cout << fu.get() << std::endl;          // 当promise还没有值的时候在此等待
	std::cout << std::this_thread::get_id() << std::endl;
	t.join();
	return 0;
}

std::async

       其实这个函数是对上面的对象的一个整合,async先将可调用对象封装起来,然后将其运行结果返回到promise中,这个过程就是一个面向future的一个过程,最终通过future.get()来得到结果。它的实现方法有两种,一种是std::launch::async,这个是直接创建线程,另一种是std::launch::deferred,这个是延迟创建线程(当遇到future.get或者future.wait的时候才会创建线程),这两个参数是std::async的第一个参数,如果没有使用这个两个参数,也就是第一个参数为空的话,那么第一个参数默认为std::launch::async | std::launch::deferred,这个就不可控了,由操作系统根据当时的运行环境来确定是当前创建线程还是延迟创建线程。那么std::async的第二个参数就是可调用对象的名称,第三个参数就是可调用对象的参数。 

#include <iostream>
#include <thread>
#include <mutex>
#include <future>

using namespace std;
int func(int i)
{
	return 10 + i;	
}
int main() 
{
	std::future<int> fu = std::async(std::launch::async, func, 6); 
	cout << fu.get() << endl;
	return 0;
}

等待自多个线程

如果需要多余一个线程等待同一个事件则需要使用std::shared_future来代替。

有时间限制的等待

前面介绍所有阻塞都会调用一个不确定的时间段,挂起线程直至等待的事件发生。在许多情况下是没问题的,但在某些情况下你会希望给等待时间加一个限制。

有两类可供指定的超时:一为基于时间段的超时,即等待一个指定的时间长度(例如30ms)或者绝对超时,即等到下一个时间点(例如2022.01.29 10:10:10)。

处理基于时间段超时的变量具有_for后缀,处理绝对超时的变量具有_until后缀。

例如:std::condition_variable具有两个重载版本的wait_for()成员函数和两个wait_unitil()成员函数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

波雅_汉库克

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

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

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

打赏作者

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

抵扣说明:

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

余额充值