优先考虑基于任务的编程而非基于线程的编程

优先考虑基于任务的编程而非基于线程的编程

”优先考虑基于任务的编程而非基于线程的编程。对比基于线程的编程方式,基于任务的设计为开发者避免了手动线程管理的痛苦(这种调用方式将线程管理的职责转交给C++标准库的开发者),并且自然提供了一种获取异步执行程序的结果(即返回值或者异常)的方式。“
–《Effective Modern C++》Item 35

先来看一下基于任务的编程,在编码方式长什么样,如下代码所示:

#include 
 
int doAsyncWork() {
	// some work
	return 10;
}

int main() 
{
    auto ans = std::async(doAsyncWork);   // async 返回的是一个 future 对象
	std::cout << ans.get() << std::endl;  // future 对象的 get 方法获取异步执行程序的结果(返回值)
}

如上代码片段所示,基于任务的并发编程方法通过调用 async 函数并指定任务的函数入口(上述 doAsyncWork 函数),就开启了一个异步任务,并且 async 函数会返回一个 future 对象,用以捕获异步执行程序的返回值。

接下来先了解基于任务的并发编程的相关 API 的基本用法,以及在使用上的一些注意事项,然后对比总结基于任务和基于线程的两种并发编程方式。

async 的基本用法

该网站上对于 async 函数的声明如下(基于C++11),不用关心它复杂的函数声明,在使用时只需要清楚以下两点:

  • 有两个重载版本,区别在于下面的原型2比原型1在第一形参位置多了一个任务“启动策略”的参数,下文会介绍这个参数的作用;其他参数表示的含义都相同;对于原型1,第一个形参为一个可调用对象,第二个参数为可变参数。
  • 返回值为一个 future 类型的变量。
// 原型1
template <class Fn, class... Args>  
future<typename result_of<Fn(Args...)>::type>    
	async (Fn&& fn, Args&&... args);

// 原型2
template <class Fn, class... Args> 
future<typename result_of<Fn(Args...)>::type> 
	async (launch policy, Fn&& fn, Args&&... args);

与 thread 显示的创建一个线程来执行任务不同,调用 async 函数(默认调用方式,上述原型1)并不一定会开启一个新的线程。若想确保使用 async 函数会开启一个新的线程执行任务,需要传入参数(原型2)std::launch::async 。 此外,async 还有一种”启动策略“,延迟启动;在 async 中传入第一个参数 std::launch::deferred。对于 async 的默认”启动策略“(即不显示的传入”启动策略“的参数)为std::launch::async | std::launch::deferred。表示 async 会自动在两者之间选择,这取决于系统和库的实现,通常会针对系统中当前的并发可用性进行优化。

按照眼见为实的常理,应该写一段代码测试一下 async 函数,验证它在“某些情况下”不会开启一个新的线程执行任务,可惜暂时没能找到合适的示例,“暂且就这样认为吧”。

使用 async 的注意事项

async 和 thread 的另一个重要区别为,async 没有将创建的线程 detach 的机制,因此在调用 async 的作用域中,会等待 async 执行的异步任务完成之后才会离开其作用域。下面用一个示例说明该区别。

int doAsyncWork()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "start doAsyncWork" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "end doAsyncWork" << std::endl;

    return 10;
}

void case1()
{
    std::thread t1(doAsyncWork);
    std::cout << "main thread end" << std::endl;
    t1.join();
}

void case2()
{
    auto ans = std::async(doAsyncWork);
    std::cout << "main thread end" << std::endl;
}

/*
	case1 和 case2 的输出一样:
	main thread end
	start doAsyncWork
	end doAsyncWork
*/

如上case2所示,使用 async 进行并发编程时,在调用 async 的作用域中,会等待 async 执行的异步任务完成之后才会离开其作用域,等价于case1中在离开作用域前调用 thread 对象的 join 方法(上述第15行代码)。

接下来看看 async 的返回值。async 的返回值是一个 future 类型的变量,用来捕获传递给 async 的函数对象的返回值或者抛出的异常。

在上面关于 async 的示例中,都使用了 auto 自动类型推导来定义一个变量接收 async 的返回值。若不使用一个变量来接收 async 的返回值,async 就变成了同步操作了,即需要等待传递给 async 的函数对象执行完成后,才会继续向下执行。因此在使用 async 进行并发编程中,一定要捕获其返回值。

先看看 future 对象的基本用法,然后再尝试从语言层面理解 future 的设计。

future 的基本用法

在上文说到,”async 的返回值是一个 future 类型的变量,用来捕获传递给 async 的函数对象的返回值或者抛出的异常。“ 因此,站在一个更高的抽象角度来看,future 类型的变量用来捕获 async 中函数对象在运行过程中的某些状态(下文中的 shared state)。

在API的使用上,若函数对象运行结束,使用 future 的 get 成员方法获取函数对象的返回值,如下示例所示:

int doAsyncWork()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "start doAsyncWork" << std::endl;   
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "end doAsyncWork" << std::endl;

    return 10;
}

int main()
{
    auto ans = std::async(doAsyncWork);
    std::cout << ans.get() << std::endl;
    std::cout << "main thread end" << std::endl;
    // std::cout << ans.get() << std::endl;
}

/*
运行输出为:
start doAsyncWork
end doAsyncWork
10
main thread end

注释第13行,取消注释16行,运行输出为:
main thread end
start doAsyncWork
end doAsyncWork
10
*/

对于 future 的 get 成员函数,有两点需要注意:

  • 调用 get 函数和 调用 thread 对象的 join 函数类似,会阻塞当前线程,直至 async 指定的函数对象运行完成。
  • get 函数只能调用一次,再次调用会抛出 std::future_error 异常。

若在 async 指定的函数对象中有异常抛出,get 函数可以提供抛出异常的访问,而基于 thread 的方法,若在线程运行中有异常发生且没有捕获,程序会直接终止(通过调用std::terminate)。如下示例所示:

#include 
#include 
#include 
 
int doAsyncWork() {
    std::cout << "start doAsyncWork" << std::endl;
    
    throw std::runtime_error("running error.");

    std::cout << "end doAsyncWork" << std::endl;
}

int main() 
{
    std::thread t1(doAsyncWork);
    t1.join();

    std::cout << "main thread end" << std::endl;
}

/*
	运行结果:
	start doAsyncWork
	terminate called after throwing an instance of 'std::runtime_error'
	  what():  running error.
	Aborted
*/

而基于任务 (async) 的方法,因为 std::async 返回的_future_提供了 get 函数(从而可以获取返回值),当线程对象中异常抛出时,get 函数可以提供异常的访问。如下示例所示:

#include 
#include 
#include 
#include 
 
int doAsyncWork() {
    std::cout << "start doAsyncWork" << std::endl;
    
    throw std::runtime_error("running error.");

    std::cout << "end doAsyncWork" << std::endl;
}

int main() 
{
    auto ans = std::async(doAsyncWork);
    
    try {
        std::cout << ans.get() << std::endl;
    }
    catch(const std::exception& e) {
        std::cout << e.what() << std::endl;
    } 

    std::cout << "main thread end" << std::endl;
}

/*
	运行结果为:
	start doAsyncWork
	running error.
	main thread end
*/

future 类中还有 wait、wait_for、wait_until 等成员函数,文档中已有很详细的使用介绍,这里就不再赘述。

从语言层面来对比基于任务编程和基于线程的编程

先从语言层面来理解 async 和 future 的设计。下面两段关于 async 和 future 的英文描述,摘录自 cplusplus.com。

The async function temporarily stores in the shared state either the threading handler used or decay copies of fn and args (as a deferred function) without making it ready. Once the execution of fn is completed, the shared state contains the value returned by fn and is made ready.

A future is an object that can retrieve a value from some provider object or function, properly synchronizing this access if in different threads. “Valid” futures are future objects associated to a shared state. The lifetime of the shared state lasts at least until the last object with which it is associated releases it or is destroyed.

基于任务(async)和基于线程(thread)的并发并发编程从编码模式上看,thread 显示的开启一个线程,并需要显示的处理 thread 的负载均衡、资源释放等问题;而 async 以一种隐式的方式开启一个异步任务,”看不到线程“,从编码模式上看,和同步编程的模式类似,auto res = async(fn) 就像一个正常的函数调用,启动一个异步任务,然后获取返回值,对于线程的管理交给标准库来处理。

你可能感兴趣的:(cpp,并发编程,c++)