1.1 启动线程
#include
。管理线程的函数和类在
中声明,保护共享数据的函数和类在其他头文件中声明std::thread
对象。创建一个有参线程对象时,该线程就开始执行,参数顺序依次为:函数名、参数列表std::thread th1(fun1,x1,x2)
fun_object f;
std::thread my_thread(f); // 1
std::thread my_thread((fun_object())); // 2
std::thread my_thread{fun_object()}; // 3
1.2 线程连接与分离
join
),还是让线程自主运行(通过分离它detach
)。如果线程对象销毁之前还没有做出决定,线程就会终止(std::thread
的析构函数会调用std::terminate()
),因此有必要确保线程即使出现异常的情况下也能正确的连接或分离join()
的动作还清理了线程相关的存储,这使得线程对象不再与已经结束的线程有任何关联,这意味着一个线程只能调用一次join()
,且调用后的joinable()
将返回falsejoin()
代码的位置,这意味着如果在线程启动后调用join()
前有异常抛出,join()
调用会被跳过。为避免应用因为异常抛出而终止,需要在异常处理过程中也调用join()
,若在try/catch块中调用join()
使得异常处理部分过于冗长join()
一定调用的方式是使用标准的“资源获取即初始化(RAII)”,并且提供一个类,在该类的析构函数中执行join()
。当函数执行到尾部时,局部对象要按照构造的逆序销毁,此时析构函数调用,从而调用join()
detach()
会让线程在后台运行,这意味着没有直接的方法和它通信,也没法等待线程结束,没有线程对象引用它,也不能被连接。分离的线程所有权和控制回传递给C++运行时库,保证和线程相关的资源在线程退出的时候被正确地回收,分离线程常被称为守护线程join()
的使用条件,因此只有在线程对象执行joinable()
返回true时才能使用detach()
1.3 传递参数给线程函数
std::thread
构造函数的附加参数即可。在默认情况下,这些参数会被拷贝到新线程的内部存储,然后像临时值那样以右值引用传递给调用函数,就算函数的参数期待的是一个引用时仍然是这样void f(std::string const& s);
void m_f(){
char buffer[10];
std::thread t(f,std::string(buffer));
t.detach();
}
std::thread
的构造函数无视函数期待的参数类型,盲目地拷贝提供地变量,且内部代码会将拷贝的参数以右值地方式进行传递。由于无法传递一个右值到非常量引用的函数参数,所以需要使用std::ref
将参数包装成引用的形式void f(widget_data& data);
void m_f(){
widget_data data;
std::thread t(f,std::ref(data));
t.join();
}
std::thread
构造函数和std::bind
的操作是根据相同的机制定义的,也可以传递一个成员函数指针作为线程函数,此时需要提供一个对象指针作为第一个参数std::thread t(&X::X_fun,&my_x);
std::unique_ptr
同一时间只允许一个实例指向一个对象,且当该实例销毁时,指向的对象也被删除),移动构造函数和移动赋值操作符允许一个对象的所有权在多个实例中转移,转移后原对象会留下空指针。这种类型的对象作为函数的参数或从函数返回时,当原对象是一个临时值时,移动自动完成,当原对象是一个命名变量时,需要调用std::move()
来请求转移void f(std::unique_ptr<m_T>);
std::unique_ptr<m_T> p(new m_T);
std::thread t(f,std::move(p));
1.4 转移线程所有权
std::ifstream
,std::unique_ptr
,std::thread
都是可移动,但是不可拷贝。临时的线程对象直接赋值会隐式调用移动操作,若对象已有关联线程,再将另一个线程的所有权转给该线程时,系统会直接调用std::terminate()
终止程序运行void f1();
void f2();
std::thread t1(f1);
std::thread t2=std::move(t1);
t1=std::thread(f2);
std::thread t3;
t3=std::move(t2);
t1=std::move(t3); // 程序崩溃
std::thread
支持移动,意味着线程的所有权可以在函数外进行转移,此时函数的返回值类型为std::thread
。同样,若要将线程所有权转移到某个函数中,可以让该函数接受一个按值传递的std::thread
实例作为一个参数,注意对命名变量要用std::move()
进行传递std::thread
支持移动的一个好处是可以构建thread_guard
类,让该类拥有线程的所有权,这避免了当thread_guard
的生命周期长于它所引用的线程时出现问题,且一旦所有权转移到这个对象后,这个线程不能再被连接或分离。在C++20中,加入了std::jthread
类,会在析构函数中自动连接class thread_guard{
std::thread t;
public:
explicit thread_guard(std::thread t_):t(std::move(t_)){
if(!t.joinable()) throw std::logic_error("No thread");
}
~thread_guard(){
t.join();
}
thread_guard(thread_guard const&)=delete;
thread_guard& operator=(thread_guard const&)=delete;
}
1.5 标识线程
std::thread::hardware_concurrency()
返回执行程序真正可以并发的线程数,在一个多核系统中返回值是CPU的核数,当系统信息无法获取时可能返回0,在线程间划分任务时它是一个非常有用的标准std::thread::id
,有两种方式进行检索:① 可以通过线程对象的成员函数get_id()
来获取,如果线程对象没有与任何执行线程相关联,get_id()
返回默认构造的std::thread::id
对象,表示没有任何线程;② 调用std::this_thread::get_id()
可以获得当前线程的标识std::thread::id
类型的对象可以随意拷贝和比较,如果两个std::thread::id
类型的对象相等,那它们代表了同一线程,或都没有任何线程。std::thread::id
也可以作为无序关联容器的键2.1 共享数据面临的问题
2.2 使用互斥锁(mutex)保护共享数据
std::mutex
实例创建互斥锁,通过成员函数lock()
对互斥锁上锁,unlock()
解锁。实践中不推荐直接调用lock()
函数,因为这样就必须在每个函数出口都要调用unlock()
,包括发生异常的情况。C++标准库为互斥锁提供了一个RAII语法的std::lock_guard
类模板,在构造时锁住提供的互斥锁,在析构时进行结束,从而保证了一个锁住的互斥锁能被正确解锁。std::mutex
和std::lock_guard
都在头文件
中声明std::mutex m; //互斥锁
std::lock_guard<std::mutex> lk(m); //在构造函数中上锁,析构函数中解锁
C++17中添加了类模板参数推导的新特性,使得像
std::lock_guard
这样的简单类模板,可以省略模板参数列表
2.3 死锁
std::lock
函数可以一次性锁住两个或更多互斥锁,且没有死锁风险。首先使用std::lock
函数锁住两个互斥锁,然后创建两个std::lock_guard
实例接过互斥锁的所有权,且提供额外参数std::adopt_lock
告诉std::lock_guard
实例互斥锁已经上过锁了,不用再在构造函数里上锁std::lock(m_1,m_2);
std::lock_guard<std::mutex> lk_1(m_1,std::adopt_lock);
std::lock_guard<std::mutex> lk_2(m_2,std::adopt_lock);
std::lock
给互斥锁上锁时提供了要么全做要么不做的语义。当std::lock
成功地获取了一个互斥锁上的锁,并且尝试从另一个互斥锁上获取锁时,如果有异常抛出,第一个锁会自动释放std::scoped_lock<>
,它的功能与std::lock_guard<>
完全等价,但是模板参数个数可变,其构造函数提供的互斥锁使用与std::lock
相同的算法上锁std::scoped_lock<std::mutex,std::mutex> lk(m_1,m_2);
std::lock
和std::scoped_lock<>
可以在需要一起获取多个锁的情况下避免死锁,但是没有办法分别获取锁。并且死锁不仅仅发生在锁上,尽管这是最常见的原因。基于以上情况,需要通过开发的规则来写出无死锁代码std::lock
来避免死锁;② 避免在持有锁时调用用户提供的代码;③ 使用固定顺序获得锁;④ 使用锁的层次结构:将互斥锁划分为多个层级,若一个线程已拥有锁,则只能获取更低层级的锁,否则抛出异常,其中当前线程所拥有的层级值为thread_local
修饰的静态变量,在每个线程中只有一份2.4 更灵活的互斥锁
std::unique_lock
模板,与std::lock_guard
一样,也是互斥锁类型参数化的类模板,也提供了相同的RAII风格的锁管理。但std::unique_lock
实例可以不拥有与其关联的互斥锁(构造函数后保持解锁状态),为了存储这个是否拥有锁的信息,std::unique_lock
实例相比于std::lock_guard
会占用更多的空间,且性能更低std::unique_lock
实例时,若将std::adopt_lock
作为第二个参数传递给构造函数,表示传递的互斥锁已经上锁,仅把互斥锁对象传递给该实例(与std::lock_gurad
一样)。若将std::defer_lock
作为第二个参数传递给构造函数,表示互斥锁在构造时保持解锁状态,可通过调用std::unique_lock
对象的lock()
成员函数,或将std::unique_lock
对象传递给std::lock
来获取锁std::unique_lock
实例不一定要拥有与之关联的互斥锁,互斥锁的所有权可以通过移动操作在不同实例中进行转移。std::unique_lock
可移动但不可赋值,若源值是一个右值(某种临时值),所有权的转移是自动的,若源值为左值(实际值或引用),则必须显式调用std::move()
实现转移std::unique_lock
支持与互斥锁相同的用于上锁和解锁的基本函数集,可以使用unlock()
成员函数来放弃拥有的锁,还可以与std::lock
泛型函数一起使用。若函数中明显不再需要该锁,可在std::unique_lock
实例被销毁前提前释放锁2.5 保护共享数据的替代措施
2.5.1 初始化期间保护共享数据
std::once_flag
和std::call_once
来处理这种情况。不同于锁住互斥锁并显式地检查指针是否初始化,std::call_once
可以在每个线程中使用,当std::call_once
返回时,指针已经被某个线程初始化了。必要的同步数据存储在std::once_flag
实例中,每个实例对应一个不同的初始化。使用std::call_once
比显式使用互斥锁消耗的资源更少,特别是在初始化完成后std::shared_ptr<X> x;
std::once_flag init_flag;
void init_x(){
x.init(new X);
}
void fun(){
std::call_once(init_flag,init_x);
x->do_something();
}
std::call_once
中第一个参数为std::once_flag
实例,第二个参数为只调用一次的函数或仿函数。常见情况下,std::once_flag
和用于待初始化的资源作为同一个类的成员出现,std::once_flag
实例和std::mutex
实例一样不能拷贝和移动std::call_once
的替代方案class X;
X& get_instance(){
static X x;
return x;
}
2.5.2 保护很少更新的数据
std::mutex
来保护数据显得用力过猛,因为在没有发生修改时,也会无法并发读取。需要使用另一种不同的互斥锁,被称为“读者-写者”锁(读写锁),他允许两种不同的用法:① 一个写者线程独占访问;② 多个读者线程并发访问std::shared_timed_mutex
,C++17又提供了std::shared_mutex
,但在C++11中什么都没有,需要使用Boost库中的互斥锁。这两种互斥锁中,std::shared_timed_mutex
支持更多的操作方式,而std::shared_mutex
可提供更高的性能std::lock_guard
和std::unique_lock
上锁,保证独占访问。对于不需要更新的操作,可以使用std::shared_lock
上锁,获得共享访问。如果任一线程拥有共享锁,试图获取独占锁的线程将阻塞2.5.3 递归锁
std::mutex
,一个线程试图锁住它已经拥有的互斥锁会发生错误,但在某些情况中,一个线程需要在没有释放互斥锁的情况下多次获取它。为此,C++标准库提供了一种互斥锁std::recursive_mutex
(递归锁),在递归锁被另一个线程锁定之前,必须释放所有的锁,使用std::lock_gurad
和std::unique_lock
会自动处理3.1 等待一个事件或其他条件
std::this_thread::sleep_for()
函数休眠很短的时间,休眠期间将互斥锁解锁,休眠结束后重新上锁,这样很难确定正确的休眠时间;③ 使用C++标准库中的条件变量(condition variable)std::condition_variable
和std::condition_variable_any
,这两个实现都包含在
库中,都需要与一个互斥锁一起才能工作。前者仅限于使用std::mutex
,而后者可以使用任何满足类似于互斥锁的最低标准的对象,但有额外的成本,除非需要额外的灵活性,否则首选std::condition_variable
std::mutex mut;
std::queue<Data> data_q;
std::condition_variable data_c;
void data_push(){
Data const data=prepare_data();
{
std::lock_guard<std::mutex> lk(mut);
data_q.push(data);
}
data_c.notify_one();
}
void data_pop(){
while(true){
std::unique_lock<std::mutex> lk(mut);
data_c.wait(lk,[]{
return !data_q.empty();
});
if(!data_q.empty()){
Data data=data_q.front();
data_q.pop();
lk.unlock();
process(data);
}
}
}
std::lock_guard
来保护队列,并把数据推入队列中,在解锁之后条件变量调用notify_one()
成员函数通知等待线程,这样使得线程A醒来之后不用再被阻塞等待互斥锁。在线程A中,首先用std::unique_lock
锁住互斥锁,然后再条件变量上调用wait()
成员函数,并传入锁对象和等待条件wait()
函数会检查等待条件,当条件为true时返回,当条件为false时解锁互斥锁,并将这个线程置于阻塞或等待状态。当线程B调用notify_one()
通知条件变量时,线程A从睡眠状态中醒来,加锁互斥锁,并再次检查条件是否满足。当线程A重新获得互斥锁并检查条件时,如果它不是直接响应来自线程B的通知,则称为伪唤醒(spurious wakeup)3.2 使用期望(future)等待一次性事件
3.2.1 从后台任务返回值
库头文件中:① 唯一期望std::future<>
;② 共享期望std::share_future<>
。一个事件只能有一个std::future<>
实例引用,但可以有多个std::share_future<>
实例引用。尽管期望用于线程间通信,但期望对象本身不提供同步访问,如果多个线程访问一个期望对象,需要通过互斥锁或其他同步机制保护访问std::thread
并没有提供一种简单方法从这样的任务中返回一个值,此时需要使用
头文件中的std::async
函数模板。std::async
返回一个std::future
对象,它将最终持有函数的返回值,当你需要该值时,只需调用get()
成员函数,此时线程就会阻塞,直到期望就绪后返回该值。std::async
的传参方式与std::thread
相同std::future<int> x=std::async(int_fun);
do_something_other();
std::out<<x.get()<<std::endl;
std::async
的附加参数中可以指定是启动要给新线程,还是同步执行任务,该参数的类型是std::launch
,当参数值为std::launch::defered
时,表明函数调用被推迟到wait()
或get()
调用时才执行,参数值为std::launch::async
时,表明函数必须在它自己的线程上运行。默认情况下,会让具体实现来选择使用哪种方式std::async
并不是将std::future
与任务联系起来的唯一方法,还可以通过将任务包装到std::packaged_task<>
类模板的实例中,或通过编写代码使用std::promise<>
类模板显式地设置值来实现3.2.2 将任务封装为与期望关联的对象
std::packaged_task<>
将期望绑定到函数或可调用对象,当调用std::packaged_task<>
对象时,它调用关联的仿函数,并让期望就绪,返回值存储为关联的数据。std::packaged_task<>
类模板的模板参数是个函数签名,构建实例时,传递的仿函数类型不必精确匹配,支持隐式转换,实例调用get_future()
成员函数返回期望std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool if_shotdown();
void deal_message();
void q_pop(){
while(!if_shotdown()){
deal_message();
std::packaged_task<void()> task;
{
std::lock_guard<std::mutex> lk(m);
if(tasks.empty()) continue;
task=std::move(tasks.front());
tasks.pop_front();
}
task();
}
}
std::thread q_pop_thread(q_pop);
//向q_pop_thread线程发送任务,根据返回的期望判断任务是否完成
template<typename Func>
std::future<void> q_push(Func f){
std::package_task<void()> task(f);
std::future<void> res=task.get_future();
std::lock_guard<std::mutex> lk(m);
tasks.push_back(std::move(task));
return res;
}
3.2.3 使用承诺设置复杂任务的期望
std::promise<>
显式设置值来创建期望(前面两种方式都是在函数执行完成之后期望就绪,当前方式是手动设置期望就绪)。std::promise
实例通过get_future()
成员函数可以获得期望对象,通过set_value()
成员函数设置类型为T的承诺(promise)值,该函数调用后期望就绪3.2.4 用期望存储异常
std::async
调用函数时,或将函数包装在std::packaged_task
中调用时,如果调用的函数抛出异常,那么该异常将存储在期望中,并会在调用get()
时抛出。std::promise
可以调用set_exception()
成员函数来使期望存储异常,若异常类型未知,可将该函数放在catch块中,传入std::current_exception()
作为参数检索抛出的异常,若异常类型已知,可用该函数提早存储新的异常而不用抛出extern std::promise<double> my_promise;
try{
my_promise.set_value(get_double());
}catch(...){
my_promise.set_exception(std::current_exception()); //检索抛出的异常
}
//提早存储新的异常
my_promise.set_exception(std::copy_exception(std::logic_error("foo")));
3.2.5 多个线程访问同一个期望
std::future
建模了异步结果的唯一所有权,使得只能有一个线程可以检索值,因为在第一次调用get()
之后就没有值可以检索了。如果并发代码要求多个线程可以等待同一事件,需要使用std::shared_future
,std::future
所有权只能在实例之间移动,但std::shared_future
实例是可以拷贝的std::shared_future
对象,其成员函数仍然是不同步的,为了避免多个线程访问它时出现数据竞争,必须用锁保护访问。常见的方法时将std::shared_future
对象的副本传递给每个线程,这样多个线程访问共享的异步状态是安全的std::shared_future
实例是由引用该状态的std::future
实例构造的,必须使用std::move
转移所有权,或者使用share()
成员函数创建一个新的std::shared_future
并直接将所有权转移给它std::promise<int> p;
std::future<int> f(p.get_future());
std::shared_future<int> sf(std::move(f));
std::shared_future<int> sf(p.get_future()); //隐式转移所有权
auto sf=p.get_future().share();
3.3 有时间限制的等待
_for
后缀,处理后者变体加_until
后缀。线程库中使用的所有C++时间处理设施,都在std::chrono
命名空间中std::condition_variable
变量有两个重载的wait_for()
成员函数和两个重载的wait_until()
成员函数,对应wait()
的两个重载:① 第一个重载只是等待,直到被信号通知,或超时过期,或一个伪唤醒发生;② 另一个重载在唤醒时检查所提供的谓词,在谓词为真时或超时过期时返回now()
获取;② 该时钟类返回时间的值类型:通过typedef
定义为该类的time_point
类型;③ 时钟计次周期:通过typedef
定义为该类的period
类型;④ 时钟是否稳定:当时钟不可调且计次速度同一,则为稳定时钟,如std::chrono::system_clock
不稳定,std::chrono::steady_clock
稳定std::chrono::duration<>
类模板负责,第一个模板参数是表示的类型,第二个模板参数是个分数,指定时长单位是多少秒,它的类型是std::ration<>
,例如chrono::duration> d(2)
表示该时长是2个1/3秒。标准库中提供了一系列预定义的时长:nanoseconds
,microseconds
,milliseconds
,seconds
,minutes
,hours
C++14中引入的
std::chrono_literals
命名空间中有许多预定义的用于时长的字面值后缀操作符,可以简化使用硬编码时长值得代码,如24h
,15min
,30ms
等。当与整型字面值一起使用时,这些后缀等同于预定义得时长类型定义,但是与浮点字面值一起使用时,会自动进行缩放而无法保证精度
std::chrono::duration_cast<>
。时长支持算术运算,可以通过加减获得新的时长,可以通过成员函数count()
获得时长内的单位数量的计数std::chrono::milliseconds ms(54832)
std::chrono::seconds s=std::chrono::duration_cast<std::chrono::seconds>(ms); //截断后结果为54
std::future_status::timeout
,若期望就绪,返回std::future_status::ready
,若期望对应的任务推迟,返回std::future_status::deferred
。库内部基于时长的等待会使用稳定的时钟来度量时间std::future<int> f=std::async(my_task);
if(f.wait_for(std::chrono::milliseconds(30))==std::future_status::ready) do_something(f.get());
std::chrono::time_point<>
类模板的实例来表示,实例的第一个参数用来指定所要使用的时钟,第二个参数用来表示计量单位。一个时间点的值是时钟从该纪元(如1970年1月1日00时)以来的时间长度,纪元是一个时钟的基本属性。可以给时间点实例增减时长得到一个新的时间点,也可以从同一个时钟的一个时间点中减去另一个时间点得到一个时长3.4 使用操作的同步来简化代码
3.4.1 使用期望的函数式编程
3.4.2 使用消息传递来同步操作
3.4.3 延续风格的并发
std::experiment
命名空间中提供了新版本的std::promise
和std::packaged_task
,其返回实例类型为std::experimental::future
,该类型拥有新特性——延续std::future
,必须等待期望准备就绪,或者使用完全阻塞的wait()
成员函数。而延续可以使得数据准备好了就进行处理,用于向期望添加延续的成员函数为then()
std::future
一样,std::experimental::future
存储的值也只能被检索一次,如果那个值被一个延续消费了,别的代码就不能再访问它。因此,当一个延续用future.then()
被添加时,原来的期望就失效了,该方法的调用会返回一个新的期望来保存延续调用的结果std::experimental::packaged_task<int()> fun_1;
std::string fun_2(std::experimental::future<int> future_1);
auto future_1=fun_1();
auto future_2=future_1.then(fun_2);
fun_2
延续函数计划在初始期望继续时在未指定的线程上运行,使得可以通过经验更好地指定线程的选择。延续函数的参数已经由库定义为一个就绪的期望,其中包含触发延续的结果。延续链接的期望可能最终持有一个值或一个异常,从延续逃出的异常将存储在保持延续结果的期望中std::async
的等价函数,可以手动编写一个扩展函数exper_async
,通过std::experimental::promise
获得一个期望,然后生成一个新线程运行lambda,该线程承诺的值设置为所提供函数的返回值template<typename Func>
std::experimental::future<decltype(std::declval<Func>()())> exper_async(Func&& func){
std::experimental::promise<decltype(std::declval<Fun>()())> p;
auto res=p.get_future();
std::thread t(
[p=std::move(p),f=std::decay_t<Func>(func)]()mutable{
try{
p.set_value_at_thread_exit(f());
}catch(...){
p.set_exception_at_thread_exit(std::current_exception());
}
}
);
t.detach();
return res;
}
3.4.4 等待多个期望
std::experimental::when_all
可以避免上述等待和切换,将等待的期望集合传递给when_all
,当集合中所有的期望都准备好时,就会返回一个新的处于就绪状态的期望,该期望可以与延续一起使用来安排额外的工作std::experimental::future<FinalResult> process_data(std::vector<MyData>& vec){
size_t const chunk_size=whatever;
std::vector<std::experimental::future<ChunkResult>> results;
for(auto begin=vec.begin(),end=vec.end();begin!=end){
size_t const remaining_size=end-begin;
size_t const this_chunk_size=std::min(remaining_size,chunk_size);
result.push_back(spawn_async(process_chunk,begin,begin+this_chunk_size));
begin+=this_chunk_size;
}
return std::experimental::when_all(results.begin(),results.end()).then(
[](std::future<std::vector<std::experimental::future<ChunkResult>>> ready_results){
std::vector<std::experimental::future<ChunkResult>> all_results=ready_results.get();
std::vector<ChunkResult> v;
v.reserve(all_results.size());
for(auto& f:all_results) v.push_back(f.get());
return gather_results(v);
}
);
}
std::experimental::when_any
可以在集合中任何一个期望准备好时,创建一个新的期望,且该期望相较于when_all
创建的期望增加了一层,它将集合与一个索引值组合进std::experimental::when_any_result
类模板的一个实例,这个索引值指示哪个期望触发了组合的期望变成就绪状态3.4.5 锁存器和屏障
std::experimental::latch
是基本的锁存器类型,来自于
头文件,其构造函数的唯一参数为初始计数器的值,当等待的事件发生时,在锁存器对象上调用count_down()
,当计数器为0时锁存器就准备好了,在锁存器对象上调用wait()
来等待锁存器准备好,调用is_ready()
检查它是否准备好,调用count_down_and_wait()
在递减计数器的同时等待计数器达到零void fun(){
unsigned const thread_count=10;
std::experimental::latch done(thread_count);
my_data data[thread_count];
std::vector<std::future<void>> threads;
for(unsigned i=0;i<thread_count;++i){
threads.push_back(std::async(std::launch::async,[&,i]{
data[i]=make_data(i);
done.count_down();
do_more_stuff();
}));
}
done.wait();
process_data(data,thread_count);
} //在std::future析构函数在函数末尾运行之前,不能保证线程已经全部完成
中提供了两种类型的屏障:std::experimental::barrier
和std::experimental::flex_barrier
,前者更基础,潜在的开销更低,后者更灵活,潜在的开销更大std::experimental::barrier
使用同步组中线程数量构造一个屏障,当每个线程完成处理后会到达屏障,通过在屏障对象上调用arrive_and_wait()
来等待组中剩余的线程,当组中最后一个线程到达时,所有线程被释放,屏障被重置。只有组内的线程可以等待屏障就绪(同步),通过在屏障上调用arrive_and_drop()
可以将线程退出组,且下一个周期必须到达屏障的线程数量减一std::experimental::flex_barrier
有一个额外的构造函数,需要传入线程数量和结束函数,当所有线程都到达屏障时,这个函数将只在其中一个线程上运行,函数体提供了串行运行的代码块,函数的返回值指定了下一周期必须到达屏障的线程数量,使用这个函数的程序员需要保证下一次到达屏障时,线程数量是正确的