前面四部分内容已经把目前常用的C++标准库中线程库的一些同步库介绍完成了,这一次我们探讨的都是C++20中的内容。主要有两个部分,信号量和latch与barrier。
由于GCC使用的libstdc++还没有完成这一部分特性,所以我们使用的是LLVM的libc++来进行实验,鉴于gcc更换标准库比较麻烦,所以我们使用的是clang编译器。在编译的时候添加选项-stdlib=libc++ -std=gnu++2a。
信号量对于信号量,我使用的比较少,大部分的时候都是在准备面试的时候,能够看到它的存在。其使用也非常的简单,感觉和锁的使用类似,都是限制更少了。他的作用是保证只有指定数目的执行体可以访问特定的资源。对于信号两,在C++中有两个类分别是std::counting_semaphore和std::binary_semaphore,他们之间的区别比较小。简单来说,std::counting_semaphore<1>就是等于std::binary_semaphore。
counting_semaphorestd::counting_semaphore是一个模板,目标有一个类型为std::ptrdiff_t(两个指针的差值,不小于17位的整数),这个不是初始的值而是运行时的最大值。构造函数可以传递初始的信号值。
std::counting_semaphore<> sema(4);使用的方法有两个acquire()和release(ptrdiff_t update = 1),从名字上也明白大致的使用方法。acquire()用于获取一个资源,release()用于释放资源,可以释放多个,但是总数不能超过最大值。
运行的函数:
int thread_fun(int thread_id) { sema.acquire(); printf("Thread Id %d Get.\n", thread_id); std::this_thread::sleep_for(1s); sema.release(); printf("Thread Id %d Release.\n", thread_id); return 0; }初始化10个线程,可以得到类似于下图的结果:
除了阻塞的acquire()之外,还有非阻塞的try_acquire与超时的try_acquire_for和try_acquire_for。
latch与barrierstd::latch与std::barrier的作用有点像起跑线,但是两者也有一定的区别。
std::latch只能用一次,如果它的计数器已经到达0,不会复原,再有线程到达,将不会阻塞。而std::barrier在计数器到达0后会复原,可以重复使用。
在使用std::latch时,可以一次性减少多次计数器,而std::barrier只能减少一次。
std::barrier是一个模板,在构造的时候可以传入一个函数类型,在计数器达到零时会执行实例化时传入函数(执行减少计数器的线程)。
latchstd::latch的构造方法很简单,即
constexpr explicit latch( std::ptrdiff_t expected );expected即需要等待的数量。
使用的方法有四个,分别是
void count_down( std::ptrdiff_t n = 1 ); bool try_wait() const noexcept; void wait() const; void arrive_and_wait( std::ptrdiff_t n = 1 );count_down是将计数器减少n但是不等待,如果n大于内部的计数器,则行为未定义;
try_wait测试是否需要等待;
wait等待计数器减到0;
arrive_and_wait等于先count_down后wait。
barrierstd::barrier是一个模板,在构造的时候可以传入一个函数类型,在计数器达到零时会执行实例化时传入函数。其构造函数为
constexpr explicit barrier( std::ptrdiff_t expected, CompletionFunction f = CompletionFunction());使用的方法也有四个,分别是:
arrival_token arrive( std::ptrdiff_t n = 1 ); void wait( arrival_token&& arrival ) const; void arrive_and_wait(); void arrive_and_drop();需要注意的是arrival_token类型,由于barrier可以使用多次,所以如果为了区分不同的阻塞,使用了arrival_token,同一批调用arrive将会得到相同的arrival_token。
在调用wait时,如果传入的arrival不为当前的批次,则将直接返回。否则等待该批次的计数器降为0。
arrive_and_drop将会减少计数器并且减少之后复原的计数器。
总结以上就是C++中的信号量,latch和barrier。使用起来比较简单,但是比较奇怪的是,latch和barrier中大部分的方法名都是相似的,或者说是相同的命名逻辑,但是不阻塞的减少在latch中是count_down,而barrier中为arrive。虽然有可能因为latch中可以传入减少的次数,然而类似的arrive_and_wait的命名却相同。
在更换库的时候,还发现了很多奇葩的地方。当时,我想使用Boost的timer库来计时,timer库分为两个版本,一个是已经被弃用的header only库,可以直接引用。另一个推荐使用的库,需要链接动态库。然而问题就在于此。原来的Boost使用的是libstdc++作为标准库编译的,而我更换了标准库后,编译阶段是没有问题的,但是在链接的时候,提示
undefined reference to `boost::timer::format(boost::timer::cpu_times const&, short, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)'