文章目录
C++11标准
本文将对C++中多线程编程进行介绍。
简介
C++标准库对并发的支持包括(从最基础、最底层到最高层的顺序):
- 内存模型——memory model:这是对内存并发访问的一组保证,主要是确保简单的普通访问能按人们的朴素预期工作
- 对无锁编程(programming without locks)的支持:这是一些避免数据竞争的细粒度底层机制
- 一个线程(thread)库:这是一组支持传统线程-锁风格的系统级并发编程的组件,如thread、conditional_variable和mutex
- 一个任务(task)支持库:这是一些支持任务级并发编程的特性:future、promise、packaged_task和async
在实际编程中,为了提高开发效率,应在尽可能高的层次上编程,尽量将复杂任务留给标准库的作者
内存模型
内存模型描述了编译器实现者和程序员之间的约定,省去程序员从机器体系结构层次思考计算机的麻烦。
内存位置
C++内存模型保证两个更新和访问不同内存位置的线程可以互不影响地执行。
如果两个线程同时访问两个属于同一个字的位域,结果是不确定的。
C++将内存位置(memory location)定义为能保证合理行为的内存单元,从而排除了单独的位域。
一个内存位置可以是一个算术类型对象、一个指针或一个非零宽度相邻位域的最大序列。
struct s {
char a; // 位置1
int b:5; // 位置2
unsigned c:11;
unsigned :0; //
unsigned d:8 // 位置3
struct {int ee:8;} e; // 位置4
};
指令重排
为了提高性能,编译器、优化器以及硬件都可能重排指令顺序。
内存序
内存序(memory ordering)用来描述一个线程从内存访问一个值时会看到什么。
数据竞争
如果两个线程同时访问同一个内存位置且之上其中之一是进行写操作时,他们就会产生数据竞争。
原子性
atomic类型
原子类型(atomic type)是atomic模板的特例化版本。原子类型的对象上的操作是原子的(atomic)。即,操作由单一线程执行,不会受到其他线程干扰。
template< class T >
struct atomic;
template< class U >
struct atomic<U*>;
atomic没有拷贝和移动操作,赋值运算符和构造函数接受包含类型T的值并访问包含值。
成员函数is_lock_free可以用来检测这些操作是否可无锁执行或者是否已用锁实现,在所有主要的C++实现中,is_lock_free对整数和指针类型都是返回true。
// 存在非内置类型的需要在链接时链接libatomic,否则会报错找不到is_lock_free
struct pod {
int a;
int b;
int c;
int d;
};
int main()
{
std::atomic_int a{2};
std::atomic<pod> B{};
std::cout << a.is_lock_free() << std::endl;
std::cout << B.is_lock_free() << std::endl;
return 0;
}
// g++ -o demo demo.cpp -latomic
// ./demo
// 1
// 0
atomic特性是为了可映射到简单内置类型的类型设计的。若类型T的对象很大,则atomic<T>
就可能用锁实现。模板参数T必须是可简单拷贝的(必须没有自定义拷贝操作)。
需要注意的是atomic变量的初始化不是原子操作,因此初始化操作可能与来自其他线程的访问操作之间产生数据竞争。
标志和栅栏
除了支持原子类型外,标准库还提供了两种更底层的同步特性:原子标志和栅栏。
他们的主要用途是实现最底层的原子特性,如自旋锁和原子类型。这两个特性是仅有的每个C++实现都保证支持的无锁机制。基本没有程序员需要使用标志和栅栏,其使用者通常是和硬件设计师紧密合作的人。
atomic_flag是最简单的原子类型,也是仅有的所有操作在任何C++实现中都保证是原子操作的原子类型。一个atomic_flag表示一条单一位信息。如需要,可以使用atomic_flag实现其他原子类型。
可以将atomic_flag想象成一种简单的自旋锁:
class spin_mutex{
atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {while(flag.test_and_set());}
void unlock() {flag.clear();}
栅栏(fence),也称为内存屏障(memory barrier),是一种根据某种指定内存序来限制操作重排的操作,除此以外不做任何其他事情。可以将其看作一种简单地减慢程序到安全速度的方法,从而令内存层次到达定义良好的合理状态。
- atomic_thread_fence(order):强制内存序为order
- atomic_signal_fence(order):强制内存序为order,用于线程以及运行于线程上的信号处理函数
volatile
说明符volatile用来描述一个对象可被线程控制范围之外的东西修改。
volatile说明符主要是要告知编译器不要优化掉明显冗余的读写操作。
除非是在直接处理硬件的底层代码中,否则不要使用volatile。同时,也不要认为volatile是一种同步机制。
线程
如果一项活动可能与其他活动并发执行,我们称之为任务(task)。线程(thread)是执行任务的计算机特性在系统层面的表示。一个标准库thread可执行一个任务。一个线程可与其他线程共享地址空间。即,单一地址空间中的所有线程能访问相同的内存位置。而并发系统编程所面临的重要挑战之一就是,确保多线程并发访问内存的方式是合理的。
thread是计算的概念在计算机硬层面的抽象。C++标准库thread的设计目标就是与操作系统线程形成一对一映射。
一个thread表示一个系统资源,一个系统线程(system thread),甚至可能有专用硬件。因此,thread可以移动但是不可以拷贝。作为一个源被移动后,thread就不再表示一个计算线程了,也就对它进行join()了。
线程的构造
thread() noexcept;
thread( thread&& other ) noexcept;
template< class F, class... Args >
explicit thread( F&& f, Args&&... args );
thread( const thread& ) = delete;
thread的构造函数是可变参数模板,着意味着为了传递给thread构造函数一个引用,我们必须使用引用包装(ref)。关于引用问题可以参考C++ 标准库——函数对象和函数适配器中关于bind的引用问题的描述。
将任务从一个thread移动到另一个thread并不影响其执行,要记住thread只是一种资源句柄,thread的移动只是改变了thread指向的是什么。
线程的析构
thread的析构函数销毁thread对象。为了防止发生系统线程的生命期长于其thread的意外情况,thread析构函数调用terminate结束程序(若thread是joinable的即get_id()!=id{})。如果希望一个系统线程在thread生命周期结束后仍然继续运行,可以使用detach从而让线程变得非joinable。
join
t.join()
告诉当前thread在t结束之前不要继续前进。
因为线程thread被看作一个资源,那么我们应该考虑使用RAII。即使用一个类来自动管理thread:
struct guarded_thread:thread{
using thread::thread;
~guarded_thread() {if (t.joinable()) t.join();}
}
detach
如果我们希望一个线程“永远活跃”直到自己结束或者由系统决定,这种线程通常称为守护线程(daemon)。使用detach可以分离一个线程从而可以使其持续运行。
但是,detach会使得称序失去对线程的管控,这往往会导致混乱。
得益于thread支持移动赋值和移动构造,可以设计出一种可以替代detach的方案:将当前thread移动到程序其他模块,通过unique_ptr或者shared_ptr来访问他们,或者将他们置于容器之中,以避免失去和他们的联系。
如果必须要detach一个thread,需要确保它没有引用其作用域中的变量,因为在thread之外你可能会对这些变量操作或者直接回收这些变量,这回导致使用它的thread异常。这实际上违反了”不要将一个局部对象的指针传递出其作用域之外“这个简单原则。
所以,当使用detach时,需要仔细考虑:
- 有没有其他更好的方法
- 需要detach的thread任务可能做什么,会有什么影响
this_thread
this_thread是一个命名空间,它提供了一些和当前正在运行线程的方法:
- get_id:获取当前线程的id
- yield:给调度器机会运行另一个thread
- sleep_util:令当前线程进入睡眠状态,直至指定time_point
- sleep_for:令当前线程进入睡眠状态,持续duration时间段
如果对系统时钟有修改(比如重置),sleep_util会受到影响,但sleep_for不会。
杀死thread
遗憾的是C++标准库没有提供一个可以杀死一个thread的通用操作,这需要我们自己去操作。
一种可能的方法(Linux下):
- 获取thread对象的native_handle——对应的pthread_t变量
- 对这个native_handle进行pthread的取消操作
thread_local
一个thread_local变量是一个thread专有的对象,其他thread不能访问,除非其拥有者将指向它的指针提供给了其他线程。
thread_local变量一般有两种使用场景:
- thread_local变量定义为局部变量,即在thread的栈中分配
- thread_local变量定义为全局变量
下面详细说明下第二种场景:
一个thread_local具有线程存储持续时间(thread storge duration),每个thread对thread_local变量都有自己的拷贝,thread_local在首次使用前初始化。如果已经构造,会在thread退出时销毁。
关于这两种场景的用法,可以参考C++11 thread_local用法
同步
数据竞争是多线程并发编程里面的主要问题,避免该问题的最好办法就是不共享数据。
将感兴趣的数据保存在局部变量中或者其他不与其他线程共享的存储中或是保存在thread_local内存中。当另一个线程需要处理这类数据时,传递数据特定片段的指针并确保在任务结束之前不触碰这些数据片段。
如果我们不能做到上面这样,就需要使用某种形式的锁机制来同步:
- 互斥量(mutex):互斥量(互斥变量,mutual exclusion variable)就是一个用来表示某个资源互斥访问权限的对象。一般路径为:获取互斥量、访问数据、释放互斥量
- 条件变量(condition variable):一个thread用条件变量等待另一个thread或计时器生成的事件。
严格来说,条件变量并不能防止数据竞争,而是帮助我们避免引入可能引起数据竞争的共享数据。
死锁
死锁产生的四个必要条件
- 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
- 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
- 循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
死锁预防
我们可以通过破坏死锁产生的4个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。
- 破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
- 破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
- 破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
死锁避免
死锁避免的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。
如果操作系统能保证所有进程在有限时间内得到需要的全部资源,则系统处于安全状态否则系统是不安全的。
- 安全状态是指:如果系统存在 由所有的安全序列{P1,P2,…Pn},则系统处于安全状态。一个进程序列是安全的,如果对其中每一个进程Pi(i >=1 && i <= n)他以后尚需要的资源不超过系统当前剩余资源量与所有进程Pj(j < i)当前占有资源量之和,系统处于安全状态则不会发生死锁。
- 不安全状态:如果不存在任何一个安全序列,则系统处于不安全状态。
互斥量
mutex对象用来表示资源的互斥访问。因此,它可以用来防止数据竞争以及同步多个thread对共享数据的访问。
互斥量:
mutex
:一个非递归互斥量;如果尝试获取一个已被获取的mutex,thread会阻塞recursive_mutex
:可被单个thread重复获取的互斥量timed_mutex
:一个非递归互斥量,提供操作(只)在指定时长内尝试获取互斥量recursive_timed_mutex
:递归限时互斥量
互斥量管理:
lock_guard<M>
:mutex M的守卫unique_lock<M>
:mutex M的锁
互斥量操作:
- lock:获取锁,线程阻塞直到获取所有权
- try_lock:尝试获取锁,返回是否获取成功的结果
- unlock:释放锁
- native_handle:返回底层实现定义的native句柄
mutex不能拷贝和移动。可以将mutex看作一种资源,而不是资源的句柄。实际上,mutex通常实现为系统资源的句柄,但由于这种系统资源不能共享、泄漏和移动,因此将它们分开考虑通常只会增加复杂性。
一个简单的死锁的例子:
std::mutex m;
template <class T>
void ShowList(T t)
{
m.lock();
std::cout << t << std::endl;
m.unlock();
}
template <class T, class... Args>
void ShowList(T value, Args... args)
{
m.lock();
std::cout << value << " ";
ShowList(args...);
m.unlock();
}
一旦线程调用了ShowList传入两个以上参数,便会发生死锁。
关于如何将这个可变参数模板函数传入thread:
模板函数只有在实例化之后才会有具体的类型,但std::thread需要一个明确的函数指针或可调用对象,而模板函数本身并不是一个具体的函数,所以直接传递模板函数名可能无法通过编译。
此时需要显示实例化模板函数:std::thread t1(ShowList<std::string, std::string>, s, s);
上面这个死锁的例子,如果用recursive_mutex替换普通mutex则可以避免死锁。
标准库提供了两个RAII类lock_guard
和unique_lock
来解决锁资源问题。
lock_guard lck{m}
:lck获取m;显式构造函数lock_guard lck{m, adopt_lock_t)
:lck保有m;假定当前thread已经获取了m;不抛出异常lock.~lock_guard()
:析构函数,对保有的互斥量调用unlock
lock_guard所实现的功能非常简单,只是对mutex实现RAII,为了获得一个提供了内含mutex上的RAII和操作的对象,应该使用unique_lock。
template< class Mutex >
class unique_lock;
基本上对mutex的操作都可以通过对应的unique_lock进行。
多重锁
为了执行某个任务获取多个锁的现象非常常见,这也是容易发生死锁的地方。
针对多重锁,标准库提供了try_lock和lock方法:
x=try_lock(locks)
:尝试获取locks的所有成员;这些锁是按顺序获取的;若所有锁获取成功,x=-1;否则x=n,其中n为不能获取的锁的数量,且不会保有任何锁lock(locks)
:获取locks的所有成员;不会产生死锁
call_once
通常我们希望初始化对象时不会产生数据竞争,为此,标准库提供了一种高效且简单的底层工具:类型once_flag和函数call_once()。
once_flag fl{}
:默认构造函数;fl未使用call_once(fl,f,args)
:若fl尚未使用,调用f(args)
可以将call_once理解为这样一种方法:它简单地修改并发前代码,这些代码依赖于已初始化的static数据。
可以用call_once或非常像call_once的机制来实现局部static变量的运行时初始化。
Color& default_color()
{
static Color def{read_from_evironment("background color")};
return def;
}
// 可能实现为:
Color& default_color()
{
static Color def;
once_flag __def;
call_once(__def, read_from_evironment, "background color");
return def;
}
条件变量
条件变量用来管理thread间的通信。一个thread可等待(阻塞)在一个condition_variable上,直至某个事件发生(如到达一个特定时刻或者另一个thread完成)。
condition_variable();
condition_variable( const condition_variable& ) = delete;
~condition_variable();
operator=[deleted]
void notify_one() noexcept; // 解除一个等待thread(如果有的话)的阻塞状态
void notify_all() noexcept; // 解除所有等待的thread的阻塞状态
void wait( std::unique_lock<std::mutex>& lock ); // lock必须被调用thread所拥有;调用原子操作lock.unlock并阻塞;当收到通知时解除阻塞,或伪唤醒;当解除阻塞时调用lock.lock()
template< class Predicate >
void wait( std::unique_lock<std::mutex>& lock, Predicate pred ); // lock必须被调用thread所拥有;while(!pred()) wait(lock)
template< class Rep, class Period >
std::cv_status wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time ); // return wait_until(lock, std::chrono::steady_clock::now() + rel_time);
template< class Rep, class Period, class Predicate >
bool wait_for( std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time,
Predicate pred ); // return wait_until(lock, std::chrono::steady_clock::now() + rel_time, std::move(pred));
template< class Clock, class Duration >
std::cv_status
wait_until( std::unique_lock<std::mutex>& lock,
const std::chrono::time_point<Clock, Duration>& abs_time ); // lock必须被调用thread所拥有;调用原子操作lock.unlock并阻塞;当收到通知或时间已达abs_time时解除阻塞;当解除阻塞时调用lock.lock();若超时,返回timeout,否则返回no_timeout
template< class Clock, class Duration, class Predicate >
bool wait_until( std::unique_lock<std::mutex>& lock,
const std::chrono::time_point<Clock, Duration>& abs_time,
Predicate pred ); // while (!pred())if (wait_until(lock, abs_time) == std::cv_status::timeout) return pred(); return true;
native_handle_type native_handle();
condition_variable可能(也可能不)依赖系统资源,因此构造函数可能因该资源缺乏而失败。但是,类似mutex、condition_variable也不能拷贝和移动,因此最好将condition_variable理解为资源本生,而非资源句柄。
当销毁一个condition_variable时,必须通知(即唤醒)所有正在等待的thread,否则他们就可能永远处于等待状态。
wait函数用condition_variable的unique_lock来防止等待thread列表上的数据竞争,以避免唤醒通知被漏掉。
普通wait(lck)是一种底层操作,其使用必须小心,且通常用于某些高层抽象的实现。此操作可能导致伪唤醒。即,系统可能决定恢复wait的thread,即使没有其他thread发出通知。显然,允许伪唤醒简化了某些系统中condition_variable的实现。我们应保证总是在循环中使用普通wait。
标准库除了提供condition_variable还提供一个condition_variable_any类,他们在功能上是等价的。condition_variable是针对unique_lock<mutex>
优化的,而condition_variable_any则可以接受任何可锁对象。
condition_variable_any的wait函数可以采用任何可锁定类型(mutex 类型,例如std::mutex)直接作为参数,condition_variable对象只能采用unique_lock<mutex>
。除此之外,它们的用法是相同的。
template< class Lock >
void wait( Lock& lock );
template< class Lock, class Predicate >
void wait( Lock& lock, Predicate pred );
任务
一个简单任务可以定义为:根据给定参数完成一项工作、生成一个结果。
C++标准库为了支持基于任务的并发模型,提供了以下特性:
packaged_task<F>
:打包一个类型为F的可调用对象,作为任务执行promise<T>
:一个对象,描述了接收类型为T的结果的目的future<T>
:一个对象,描述了类型为T的结果的源shared_future<T>
:一个future,可以从中多次读取类型为T的结果x=async(policy,f,args)
:根据policy调用f(args)x=async(f,args)
:根据默认策略调用:x=async(launch::async|launch::deferred,f,args)
future 和 promise
任务间的通信由一对future和promise处理,任务将其结果放入一个promise,需要此结果的任务则从对应的future中提取结果:
关于共享状态:
共享状态除了返回值或异常,它还包含两个thread安全交换数据所需的信息。一个共享状态最低限度应能保存:
- 一个恰当类型的值或一个异常。对于返回void的future,此值什么也不包含
- 一个就绪位(ready bit),支持是否已准备好一个值或一个异常供future提取
- 一个任务,对一个由async用策略deferred调用的thread,当对其future调用get时,会执行此任务
- 一个使用计数(use count),使得当且仅当共享状态的最后一个使用者放弃时彩销毁它。特别的,如果共享状态中保存的值具有析构函数,则当使用计数变为0时回调用此析构函数
- 一些互斥数据(mutual exclusion data),能用来将任何可能处于等待的thread解除阻塞状态(比如condition_variable)
一个C++实现可为共享状态提供下列操作:
- 构造:可能使用用户提供的分配器
- 就绪:设置就绪位,并将任何正在等待的thread解除阻塞
- 释放:递减计数器,若这是最后一个使用者,销毁共享状态
- 丢弃:若已不可能由promise将一个值或异常放入共享状态(例如,由于promise已被销毁),则一个异常future_error和错误状态broken_promise被存入共享状态中,共享状态被置为就绪
promise
一个promise就是一个共享状态的句柄,它是一个任务可用来存放其结果的地方,供其他任务通过future提取。
void task_func(std::promise<int> result_promise)
{
result_promise.set_value(11);
}
void test_promise()
{
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t(task_func, std::move(p));
// 获取结果
std::cout << "result = " << f.get() << std::endl;
t.join();
}
通过promise只能传输单一结果值,但是注意值是被移入移出共享状态的,而不是进行拷贝,所以我们可以以很低的代价传输一组对象。
packaged_task
packaged_task保存了一个任务和一个future/promise对:
将待执行的任务(一个函数或函数对象)传递给packaged_task。当任务执行到return x时,会引发在packaged_task的promise上调用set_value(x)。类似地,throw x会引发一个set_exception(px),其中px是一个指向x的exception_ptr。基本上,packaged_task像下面这样执行任务f(args):
try {
pr.set_value(f(args));
}
catch(...) {
pr.set_exception(current_exception());
}
packaged_task可以移动但不能拷贝,但packaged_task可以拷贝其任务,并假定任务的副本生成与原任务相同的结果。
丢弃一个共享状态(如析构函数和移动操作所作的)意味着令其就绪。如果尚未保存值或异常,就会保存一个指向future_error的指针。
没有对应get_future的get_promise操作,因为promise的使用完全由packaged_task处理。
一个简单例子:
int ff(int i)
{
if (i) return i;
throw std::runtime_error("ff(0)");
}
void test_packaged_task()
{
std::packaged_task<int(int)> pt1{ff};
std::packaged_task<int(int)> pt2{ff};
pt1(1);
pt2(0);
std::future<int> f1 = pt1.get_future();
std::future<int> f2 = pt2.get_future();
try {
std::cout << f1.get() << std::endl;
std::cout << f2.get() << std::endl;
} catch (std::exception &e) {
std::cout << "exception: " << e.what() << std::endl;
}
}
int main()
{
test_packaged_task()
return 0;
}
运行结果:
1
exception: ff(0)
从这个例子可以看出,使用packaged_task得到的结果与普通函数调用的结果是一样的,即使任务函数的执行和结果的get不在同一个线程中。这让我们可以关注任务本身而不必考虑thread和锁。
future
future奇偶是共享状态的句柄,它是任务提取由promise存放结果的地方。
future保存一个独一无二的值,它并不提供拷贝操作。
如果future保存了一个值,它只能被移出。因此get只能被调用一次。如果需要多次读取,应该使用shared_future。
如果哦future<T>
的值类型T为void或一个引用,则对get应采取特殊规则:
future<void>::get()
不返回值:它简单返回调用者或者抛出一个异常future<T&>::get()
返回一个T&
。引用不是对象,因此标准库必定传递的是其他什么东西,例如一个T*
,get将其转换回一个T&
我们可以调用wait_for()和wait_until()来获得future的状态。
shared_future
shared_future和future非常相似,关键差别是shared_future将其值移动到可被反复读取及共享的位置。类似future<T>
,如果shared_future<T>
的值类型T为void或引用,则对其get()应用特殊规则:
shared_future<void>::get
不返回值,它简单返回调用者或者抛出一个异常shared_future<T&>::get
返回一个T&
。引用不是对象,因此标准库必定传递的是其他什么东西,例如一个T*
,get将其转换回一个T&
- 若T不是引用,
shared_future<T>::get
返回一个const T&
async
future、promise和packaged_task可以让我们编写简单的任务而不必过于担心thread了。但是,我们仍需考虑使用多少个thread以及任务由当前thread运行来世由其他thread运行。这种决策可以交给线程启动器(thread launcher)来完成,这是一个函数,它决定是否创建一个新thread、回收一个旧thread或简单地在当前thread上运行任务。
template< class F, class... Args >
std::future</* see below */> async( F&& f, Args&&... args );
template< class F, class... Args >
std::future</* see below */> async( std::launch policy,
F&& f, Args&&... args );
async基本可以看作一个复杂启动器的简单接口。调用async返回一个future<R>
,其中R的类型与其任务的返回类型相同。
int ff(int i)
{
if (i) return i;
throw std::runtime_error("ff(0)");
}
int main()
{
std::future<int> f = std::async(ff, 2);
int i = f.get();
std::cout << "async get: " << i << std::endl;
return 0;
}
启动策略:
enum class launch : /* unspecified */ {
async = /* unspecified */,
deferred = /* unspecified */,
/* implementation-defined */
};
- async:就像创建了一个型thread执行任务一样
- deferred:在对任务的future执行get()的时刻执行任务
对于是否启动一个新thread,启动器有很大的自由裁定权力。
map-reduce并行算法
一个简单的map-reduce并行算法的步骤:
- 创建一些待运行的任务
- 并行运行这些任务
- 合并结果
可以完全使用C++标准库提供的future和async机制实现上面的并行算法。
同时QT的QtConcurrent也是对这一思想的实现。
参考
C++ 多线程同步condition_variable_any的用法
C++11中的atomic
【C++11】晦涩难懂语法系列:可变参数模板
C++11的多线程、function和bind、可变函数模板
【C++11 多线程】仔细地将参数传递给线程(三)