第 22.19.7 節

semaphore、latch 與 barrier

0瀏覽次數0訪問次數--跳出率--平均停留

C++20 在 <semaphore><latch><barrier> 中加入了三類更高層的同步工具:

std::counting_semaphore
std::binary_semaphore
std::latch
std::barrier

它們解決的問題和 mutex 不同:

  • mutex:保護共享臨界區;
  • semaphore:限制同時進入某個區域的線程數量,或表示可用資源數量;
  • latch:一次性等待若干參與者全部到達;
  • barrier:多個線程反覆進行“階段同步”。

這些工具不是併發入門第一優先級,但理解後可以避免手寫複雜的條件變量計數邏輯。

1. std::counting_semaphore

計數信號量內部維護一個計數值。

線程進入前調用:

semaphore.acquire();

如果計數大於 0:

计数减 1
线程继续

如果計數為 0:

线程阻塞等待

使用完資源後調用:

semaphore.release();

計數增加,並可能喚醒等待線程。

2. 限制併發數量

例如只允許最多兩個線程同時執行某段任務:

#include <chrono>
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>

using namespace std::chrono_literals;

std::counting_semaphore<2> slots(2);

void worker(int id)
{
    slots.acquire();

    std::cout << "start " << id << '\n';
    std::this_thread::sleep_for(500ms);
    std::cout << "end " << id << '\n';

    slots.release();
}

int main()
{
    std::vector<std::thread> threads;

    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(worker, i);
    }

    for (auto& thread : threads)
    {
        thread.join();
    }
}

即使創建了 5 個線程,真正進入受限區域的線程最多隻有 2 個。

3. 模板參數和初始計數不是同一個東西

std::counting_semaphore<10> semaphore(3);

其中:

10

表示實現至少支持的最大計數上界;

3

表示當前初始計數。

不要把兩者混在一起理解。

4. try_acquire()

如果不想阻塞,可以使用:

if (semaphore.try_acquire())
{
    // 成功获得一个许可
    semaphore.release();
}

還有帶時間限制的版本:

try_acquire_for(...)
try_acquire_until(...)

適合“等不到資源就做其他事情”的場景。

5. std::binary_semaphore

二值信號量只有兩種狀態,可以理解為計數最多為 1 的 semaphore。

標準庫提供別名:

std::binary_semaphore

例如:

std::binary_semaphore signal(0);

一個線程:

signal.acquire();

另一個線程:

signal.release();

它可以用於簡單的“一次允許一個等待者繼續”的同步。

不過二值信號量和 mutex 的語義仍然不同:

mutex 強調所有權:由獲得鎖的線程釋放;semaphore 強調許可數量,釋放許可的線程不一定是獲得許可的那個線程。

6. semaphore 不能自動保護複雜共享狀態

有了 semaphore 並不意味着共享容器就自動線程安全。

例如:

std::vector<int> data;

如果多個線程同時修改這個 vector,仍然需要正確同步。

semaphore 常用於:

控制并发数量
限制资源池容量
表示可用资源数
线程间发放许可

而不是簡單替代 mutex。

7. std::latch

std::latch 是一個一次性倒計時同步點。

創建時指定計數:

std::latch done(3);

參與者完成後調用:

done.count_down();

等待方調用:

done.wait();

當計數降到 0 後,所有等待者繼續執行。

8. latch 示例

#include <iostream>
#include <latch>
#include <thread>
#include <vector>

std::latch done(3);

void worker(int id)
{
    std::cout << "worker " << id << " done\n";
    done.count_down();
}

int main()
{
    std::vector<std::thread> threads;

    for (int i = 0; i < 3; ++i)
    {
        threads.emplace_back(worker, i);
    }

    done.wait();
    std::cout << "all workers reached the latch\n";

    for (auto& thread : threads)
    {
        thread.join();
    }
}

這裏:

done.wait();

只關心“3 個參與者是否都已經完成某個階段”。

9. count_down()wait()arrive_and_wait()

常見操作:

latch.count_down();
latch.wait();
latch.arrive_and_wait();

arrive_and_wait() 可以理解為:

我已经到达
计数减一
然后我也等待其他参与者

10. latch 只能使用一次

這是 latchbarrier 最關鍵的區別。

一旦 latch 的計數變為 0:

它不会重新恢复到初始值

所以它適合:

  • 等待多個初始化任務完成;
  • 等待一組 worker 啓動就緒;
  • 等待一次性並行任務全部結束。

如果程序需要一輪又一輪地同步,應使用 std::barrier

11. std::barrier

std::barrier 是可重複使用的階段同步點。

假設有 3 個線程,每一輪都必須:

阶段 1 全部完成
      ↓
所有线程才能进入阶段 2
      ↓
阶段 2 全部完成
      ↓
所有线程才能进入阶段 3

這種場景非常適合 barrier。

12. barrier 示例

#include <barrier>
#include <iostream>
#include <thread>
#include <vector>

std::barrier sync_point(3);

void worker(int id)
{
    std::cout << "phase 1: " << id << '\n';
    sync_point.arrive_and_wait();

    std::cout << "phase 2: " << id << '\n';
    sync_point.arrive_and_wait();

    std::cout << "phase 3: " << id << '\n';
}

int main()
{
    std::vector<std::thread> threads;

    for (int i = 0; i < 3; ++i)
    {
        threads.emplace_back(worker, i);
    }

    for (auto& thread : threads)
    {
        thread.join();
    }
}

雖然每個階段內部的打印順序仍然不確定,但不會有某個線程提前跨過同步點進入下一階段,而其他線程還沒有完成上一階段。

13. barrier 會自動開始下一輪

當本輪所有參與者到達後,barrier 會完成當前階段,然後自動為下一階段重新準備。

這正是它與 latch 的不同:

latch
一次性

barrier
可重复阶段同步

14. completion function

std::barrier 還可以在每一階段全部參與者到達後執行一個完成函數。

概念上:

所有线程到达 barrier
        ↓
执行 completion function
        ↓
进入下一阶段

這適合每輪結束都需要統一做一次狀態更新的算法。

使用這個功能時應確保 completion function 本身不會引入新的複雜阻塞或鎖依賴。

15. arrive_and_drop()

如果某個參與者之後不再參加後續階段,可以調用:

barrier.arrive_and_drop();

表示:

我完成本輪到達,並且從以後各輪的參與者數量中永久退出。

這和簡單離開線程不同,因為 barrier 必須知道未來還應該等待多少參與者。

16. 三種工具怎麼選

問題推薦工具
最多允許 N 個線程同時訪問資源counting_semaphore
一個線程給另一個線程發一個簡單許可binary_semaphore
等待 N 個一次性任務全部到達latch
多個線程每一輪都要在階段邊界會合barrier
保護共享對象的一致性mutex
等待複雜謂詞條件condition_variable

17. 和 condition_variable 的關係

這些工具很多都可以用:

mutex + condition_variable + counter

手工實現。

例如 latch 本質上就是一種:

计数递减到 0 后唤醒等待者

的同步模式。

但既然 C++20 已經提供標準抽象,就不必每次重新發明一套狀態變量和通知協議。

標準工具的優勢是:

  • 意圖更明確;
  • 代碼更短;
  • 更不容易寫錯邊界條件;
  • 閲讀代碼的人一眼能看出同步模式。

18. 常見錯誤

  1. 把 semaphore 當成 mutex,誤以為它自動保護共享對象。
  2. acquire() 後忘記 release(),導致許可永久減少。
  3. 認為 binary semaphore 具有 mutex 一樣嚴格的線程所有權語義。
  4. 把一次性的 latch 當成可以重置循環使用的同步器。
  5. barrier 的參與者數量設計錯誤,導致永遠等不到所有參與者。
  6. 某個線程提前退出,卻沒有用 arrive_and_drop() 調整 barrier 後續參與數量。
  7. 在 completion function 中執行復雜阻塞操作,讓所有參與者都卡在階段邊界。

小結

  • counting_semaphore 用計數控制可同時獲得的許可數量。
  • binary_semaphore 是計數最多為 1 的信號量語義。
  • latch 是一次性倒計時同步點,計數歸零後永久打開。
  • barrier 是可重複使用的階段同步點。
  • 這些工具解決的是同步模式,不直接替代 mutex 對共享狀態的保護。
  • C++20 項目遇到對應模式時,優先考慮標準同步原語,而不是手寫複雜條件變量計數器。
音乐页