std::thread線程庫詳解(5)


目錄

前言

前面四部分內容已經把目前常用的C++標准庫中線程庫的一些同步庫介紹完成了,這一次我們探討的都是C++20中的內容。主要有兩個部分,信號量和latch與barrier。

由於GCC使用的libstdc++還沒有完成這一部分特性,所以我們使用的是LLVM的libc++來進行實驗,鑒於gcc更換標准庫比較麻煩,所以我們使用的是clang編譯器。在編譯的時候添加選項-stdlib=libc++ -std=gnu++2a

信號量

對於信號量,我使用的比較少,大部分的時候都是在准備面試的時候,能夠看到它的存在。其使用也非常的簡單,感覺和鎖的使用類似,都是限制更少了。他的作用是保證只有指定數目的執行體可以訪問特定的資源。對於信號兩,在C++中有兩個類分別是std::counting_semaphorestd::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_fortry_acquire_for

latch與barrier

std::latchstd::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_downwait

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++中的信號量,latchbarrier。使用起來比較簡單,但是比較奇怪的是,latchbarrier中大部分的方法名都是相似的,或者說是相同的命名邏輯,但是不阻塞的減少在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能解決這一問題吧。

博客原文:https://www.cnblogs.com/ink19/p/std_thread-5.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM