c++11并发编程入门

简介

并发能够充分利用多核心处理器,但并行编程却面临着严峻的挑战。

并行编程的一个常见问题是数据同步,即多个语句同时访问同一资源,当一个线程在写,而另一个在读时,就会造成不可预料的后果。

加锁可以在避免上述问题,但使用锁本身也带来了一系列问题,如死锁、效率低下等。不良的代码也可能造成cpu空转等待等资源浪费。

在c++11之前,语言或标准库都没有对并发进行任何的支持。但c++11提供了以下支持:

  • 语言核心定义了一个内存模型,保证当更改“被两个不同线程使用”的两个object时,它们彼此独立,并引用thread_local关键字。
  • 标准库支持启动多线程,包括传递实参,返回数值、跨线程边界传递异常、同步化等,使得控制流程和数据访问同步成为可能。

标准库提供了高级接口,可以启动线程,它是架构在低层接口上的。提供的低层接口包括mutex/atomic。

高级接口async()和future

它们的功能:

  • async()提供一个接口,让函数或函数对象尝试在后台运行,成为一个独立线程
  • future允许等待线程结束并获取返回值(可能是异常)

注意,async()是尝试让函数在后台运行,不提供强制性保证。具体地说:

  • 如果有线程处于可用状态,它的确会启动
  • 如果环境不支持多线程,或者当时无线程可用,它会推迟函数的异步执行,直到程序明确要获取其结果

比如要计算两个数据的各,这两个数据是两个函数的返回值,通常的顺序型编程为:func1() + func2()。

并行处理程序如下:

#include <future>
#include <thread>
#include <iostream>
#include <chrono>
#include <random>
#include <exception>

using namespace std;

int DoSomething(char c)
{
	// 随机数生成器
	std::default_random_engine dre(c);
	std::uniform_int_distribution<int> id(10, 1000);
	
	for (int i = 0; i < 10; ++i)
	{
		this_thread::sleep_for(chrono::milliseconds(id(dre))); // 休眠随机一段时间
		cout.put(c).flush();
	}
	
	return c;
}

int func1()
{
	return DoSomething('.');
}

int func2()
{
	return DoSomething('+');
}

int main()
{
	cout << "begin..." << endl;
	std::future<int> ret1(std::async(func1)); // 异步方式启动func1,它会立即以另一线程启动或者等待
	int ret2 = func2(); // 同步方式调用func2
	int ret = ret1.get() + ret2; // 得到func1的运行结果并求和
	cout << "ret = " << ret << endl;
}

两个函数会以可视化的方式打印字符,并最终返回该字符的int值。注意以下几点:

  • std::future<int> ret1(std::async(func1));尝试异步启动func1于后台,并将结果赋值给future对象
  • async会尝试将所获得的函数立刻异步启动于一个分离的线程内,因此func1被启动了,且不会造成main的停滞
  • future是必要的,原因有2:
    • 程序需要获取异步线程的返回值
    • 它可以确保异步函数最终被调用,这个是强制性的。程序需要确保异步线程执行完毕。
  • future的get()函数确保异步函数最终被调用,具体地说,以下三种情况之一会发生:
    • 如果func1已经启动并且执行完毕,会立刻获得结果
    • 如果func1已经启动但未执行结束,get会引发停滞,等待函数执行结束后返回结果
    • 如果func1尚未启动,它会强迫启动如同一个同步调用,直到运行结束并返回结果
  • 这样比通常的顺序调用节省约func1的执行时间
  • 为了取得最佳效果,通常程序应使调用async和get之间的间隔尽量大
  • 可以使用参数强迫async不推迟目标函数的执行,如async(std::launch::async, func1),但这可能会抛出异常
  • 也可以强制延迟函数的执行,如async(std::launch::deferred, func1),只有在调用get时,函数都会被执行,适用于缓式求值的情况
  • 以上程序未考虑数据同步和异常处理
低层接口:thread/promise

  1. thread

可以直接使用thread对象启动线程:

std::thread t(DoSomething);

注意,要么线程被join,要么将它detach,否则会导致程序崩溃。

相对高级接口async,它的区别:

  • 不提供发射策略,它会立即启动一个新线程,若出错则抛出异常
  • 不能处理线程结果,只能获取唯一的线程id
  • 若线程内发生异常且未被捕捉,程序会终止
  • 线程必须被join或detach,否则会崩溃
  • 如果线程运行期间,main结束了,那么所有线程也会被硬性终止
  1. promise

用于在线程之间传递参数和处理异常,它与future配对使用。

future使用promise的get_future函数获取关联,然后,对于promise设置的值(set_value),future可以获取(get)。

示例如下:

#include <thread>
#include <future>
#include <iostream>
#include <string>
#include <exception>
#include <functional>
#include <utility>

using namespace std;

void DoSomething(std::promise<string>& p)
{
	try
	{
		char c = cin.get();
		if (c == 'x')
		{
			throw std::runtime_error(string("char ") + c + " read");
		}
		
		string s = string("char ") + c + " processed";
		p.set_value(std::move(s));
	}
	catch(...)
	{
		p.set_exception(std::current_exception());
	}
}

int main()
{
	try
	{
		std::promise<string> p;
		std::thread t(DoSomething, std::ref(p));
		t.detach();
		
		std::future<string> f(p.get_future());
		
		cout << "ret: " << f.get() << endl;
	}
	catch(...)
	{
		cout << "err" << endl;
	}
}
数据同步

数据竞险及数据不同步的问题不再重复,c++标准库提供了多种方法使程序在并发数据访问方面获得额外的保证:

  • 使用 future和promise,它们保证原子性和次序,一定是结果返回后才设定状态,不会同步读写
  • 使用mutex/lock,它提供对资源的独占权,防止对数据并发读写
  • condition variable,使线程间等待条件变量的变更,这样就保证了次序
  • atomic,原子变量,它保证对数据的访问是不可分割的

这些方式从上到下,从高级到低级,根据使用场景的不同选择使用。

关于各类的用法就不展开叙述了,可以参考相关示例。

最后

c++11对并发编程提供的支持远不止这些,可以在实践中不断深入学习。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值