目錄
前言
前面四部分內容已經把目前常用的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_semaphore
std::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與barrier
std::latch
與std::barrier
的作用有點像起跑線,但是兩者也有一定的區別。
std::latch
只能用一次,如果它的計數器已經到達0,不會復原,再有線程到達,將不會阻塞。而std::barrier
在計數器到達0后會復原,可以重復使用。
在使用std::latch
時,可以一次性減少多次計數器,而std::barrier
只能減少一次。
std::barrier
是一個模板,在構造的時候可以傳入一個函數類型,在計數器達到零時會執行實例化時傳入函數(執行減少計數器的線程)。
latch
std::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
。
barrier
std::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&)'
如果對於編譯比較熟悉的話,應該是知道這是在鏈接時找不到符號導致的錯誤,一般是沒有鏈接動態庫導致的錯誤。而比較奇怪的是,提示的只有這一個函數,如果是沒有鏈接相關的動態庫,應該是有更多的提示,而不是僅僅只有這一條。然后想到更換了標准庫,猜想應該是std::string
在編譯時符號不一致導致的。
經過驗證的確如此。通過對以下文件分別使用不同的庫進行編譯。
#include <thread>
#include <iostream>
#include <string>
#include <chrono>
#include <vector>
#include <boost/timer/timer.hpp>
using namespace std::chrono_literals;
boost::timer::auto_cpu_timer timer;
int my_func(std::string data);
int main() {
my_func("test");
return 0;
}
得到的報錯分別是:
libc++
undefined reference to `my_func(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >)'
而libstdc++
undefined reference to `my_func(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
可以看到差別,如果編譯成匯編文件,還可以看到更加詳細的差別。總之,不同的標准庫無法簡單的混編。也許Modules能解決這一問題吧。