Skip to content

Wiki

Semaphore, latch, and barrier

About 18 min read

Existing English translations were reused. 1 current source blocks remain in Simplified Chinese, without triggering paid translation.

C++20 at:

<semaphore>
<latch>
<barrier>

Several higher-level synchronization tools were added:

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

They and mutex solve different problems.

You can first remember it with one sentence:

mutex
→ 一次只允许一个线程进入临界区

semaphore
→ 一共有多少张“通行证”

latch
→ 等大家一次性全部到齐

barrier
→ 每一轮都要等大家到齐后再进入下一轮

Therefore:

semaphore
→ 许可数量控制

latch
→ 一次性同步

barrier
→ 可重复阶段同步

Many of these tools can be used:

mutex + condition_variable + counter

Manual implementation.

However, since the standard library already provides ready-made abstractions, there is no need to redesign the state, counting, and notification protocols yourself every time.


1.semaphore: number of permits

1.1.semaphore: controls the "number of permits"

1.2.std::counting_semaphore

std::counting_semaphore Internally maintains a count.

This count can be understood as:

How many passes are currently still available.

For example:

当前计数 = 3

It can be understood as:

目前还有 3 个线程可以获得许可

Called before a thread attempts to enter a restricted region:

semaphore.acquire();

If the current count:

> 0

Then:

计数减 1
线程继续执行

If the current count:

== 0

Then:

暂时没有许可
线程阻塞等待

After use:

semaphore.release();

Indicates:

Return a license.

Therefore:

计数加 1

and may wake up a thread that is waiting for permission.

So the most core process of semaphore is:

acquire()
→ 拿一张通行证

执行受限制的工作

release()
→ 把通行证还回去

1.3.counting_semaphore Most typical use: limit the number of concurrent executions

For example:

At most, only 2 threads are allowed to execute a certain task simultaneously.

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

using namespace std::chrono_literals;

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

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

    std::println("start {}", id);

    std::this_thread::sleep_for(500ms);

    std::println("end {}", id);

    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();
    }
}

Possible output:

start 0
start 1
end 0
start 2
end 1
start 3
end 2
start 4
end 3
end 4

Although we created:

5 个线程

However:

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

Initially there are only 2 licenses.

So there are at most only:

2 个线程

can simultaneously be in:

slots.acquire();
...
slots.release();

between.

It can be understood as:

一开始有 2 张通行证

线程 A 拿走一张
剩 1 张

线程 B 拿走一张
剩 0 张

线程 C 来了
没票
只能等

A 或 B 归还一张
↓
C 获得许可继续

1.4.The biggest difference between semaphore and mutex

mutex Usually expressed as:

Only one thread can enter the critical section at the same time.

And:

std::counting_semaphore<N>

Can express:

At most N threads are allowed to enter at the same time.

Therefore:

mutex
≈ 只有 1 把钥匙

counting_semaphore
≈ 有 N 张通行证

However, they also have a more important semantic distinction:

mutex
→ 强调锁的所有权

semaphore
→ 强调许可数量

For mutex:

谁 lock
通常就应该由谁 unlock

And semaphore:

谁 acquire
不要求必须由同一个线程 release

In other words:

A permit of a semaphore can be acquired by one thread and released by another thread.

This allows it not only to limit the degree of concurrency, but also to perform signaling between threads.


1.5.Template parameters and initial counts are not the same thing

For example:

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

Here:

10

No:

当前有 10 个许可

It indicates:

The upper bound of the maximum count supported by this semaphore is at least 10.

And:

3

is:

Initial count.

So when this object is first created:

semaphore.acquire();
semaphore.acquire();
semaphore.acquire();

The first three attempts can succeed directly.

Fourth time:

semaphore.acquire();

You just have to wait for others:

semaphore.release();

So remember:

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

10
→ 计数上限相关参数

3
→ 当前初始许可数量

Do not mix them together.


1.6.try_acquire()

If you don't want to:

没有许可就一直阻塞

Available:

if (semaphore.try_acquire())
{
    // 成功获得许可

    semaphore.release();
}

Its logic is:

有许可
→ 拿走一个
→ 返回 true

没许可
→ 立刻返回 false
→ 不等待

This is the same as:

mutex.try_lock();

Very similar.


1.7.acquire with timeout

Also available:

semaphore.try_acquire_for(...);

Or:

semaphore.try_acquire_until(...);

It can be understood as:

try_acquire_for()
→ 最多等一段时间

try_acquire_until()
→ 最多等到某个时间点

For example:

最多等 100ms

拿到许可
→ 继续

100ms 还没拿到
→ 返回失败
→ 执行备用逻辑

Suitable for:

资源池暂时没资源

连接数已满

并发额度暂时耗尽

不能无限等待

scenarios like this.


1.8.std::binary_semaphore

C++20 also provides:

std::binary_semaphore

It can be understood as:

A semaphore whose count is at most 1.

That is, it has only two states:

0
→ 没有许可

1
→ 有一个许可

For example:

std::binary_semaphore signal(0);

Initial:

0 个许可

Therefore:

signal.acquire();

It will block.

Another thread executes:

signal.release();

Later:

许可变成 1
↓
一个等待线程可以继续

1.9.binary_semaphore can be understood as a "gate"

For example:

std::binary_semaphore signal(0);

You can think of it as:

门一开始是关着的

Thread A:

signal.acquire();

Equivalent to:

门没开
→ 在门口等

Thread B:

signal.release();

Equivalent to:

发放一个许可
→ 让一个等待线程通过

So a binary semaphore is well suited to express:

线程 A 等一个信号

线程 B 发出信号

1.10.binary_semaphore is not a mutex

Although the count of a binary semaphore is at most 1, it looks very much like:

std::mutex

But they are not the same thing.

mutex:

强调所有权

哪个线程获得锁
通常由哪个线程释放

and binary_semaphore:

强调“有没有许可”

释放许可的线程
可以不是 acquire 的那个线程

You can remember it like this:

mutex
→ 谁锁门,谁开门

semaphore
→ 系统里有几张通行证

So do not, because of:

binary_semaphore 的值只有 0 / 1

Just treat it as a complete replacement for a mutex.


1.11.A semaphore does not automatically make a shared object thread-safe.

For example:

std::vector<int> data;

Multiple threads simultaneously:

data.push_back(...);

A data race may occur.

Even if you used:

std::counting_semaphore<4> semaphore(4);

Nor will it magically make:

std::vector

Make it thread-safe.

semaphore only solves:

最多允许多少个线程获得许可

does not automatically resolve:

共享对象的一致性保护

So a very common combination is:

semaphore
→ 控制最多多少个任务同时运行

mutex
→ 保护任务访问的共享数据结构

The responsibilities of the two are completely different.


1.12.After acquire, be sure to remember to release

For example:

semaphore.acquire();

// 工作

semaphore.release();

If it happens in between:

return

异常

复杂分支

Cause:

release()

If it is not executed, then:

许可被拿走以后永远没有归还

Available licenses will become fewer and fewer.

For example, originally:

2 个许可

After missing one release:

永远只剩 1 个

One more leak:

永远变成 0

All subsequent threads may become stuck.

Therefore, semaphores must also be carefully considered:

异常安全

提前 return

许可归还

2.latch: one-time synchronization

2.1.latch: one-shot countdown synchronization

std::latchIt can be understood as:

A countdown door that can only be used once.

For example:

std::latch done(3);

Indicates:

初始计数 = 3

For each completed participant:

done.count_down();

Decrement the count by 1.

The process is similar:

3
↓
2
↓
1
↓
0

When the count becomes:

0

The thread waiting on the latch can then continue.


2.2.What is latch best suited to express?

It is best suited for this kind of requirement:

Wait until all N tasks are completed, then continue.

For example:

启动 3 个初始化任务

任务 1 完成
→ count_down()

任务 2 完成
→ count_down()

任务 3 完成
→ count_down()

计数变成 0
↓
主线程继续

2.3.latch example

#include <latch>
#include <print>
#include <thread>
#include <vector>

std::latch done(3);

void worker(int id)
{
    std::println("worker {} done", id);

    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::println("all workers reached the latch");

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

Possible output:

worker 1 done
worker 0 done
worker 2 done
all workers reached the latch

The order of the first three lines is uncertain.

However:

all workers reached the latch

Definitely occurs at:

3 次 count_down()

After everything is completed.

So the main thread does not care at all:

哪个 worker 先完成

It only cares about:

计数归零了吗?

2.4.count_down()

done.count_down();

Indicates:

I finished my share of the work and subtracted the count.

For example:

初始 3

第一个线程 count_down()
→ 2

第二个线程 count_down()
→ 1

第三个线程 count_down()
→ 0

When the count reaches:

0

latch is permanently opened.


2.5.wait()

done.wait();

Indicates:

If the latch count has not yet reached zero, wait.

If at this point:

count > 0

Thread blocking.

If already:

count == 0

Then:

直接返回

Moreover, once latch becomes 0:

以后一直保持“打开”

will not close again.


2.6.arrive_and_wait()

If the current thread:

既是参与者

又要等待其他参与者

Can be used:

latch.arrive_and_wait();

It can be understood as:

我已经到达
↓
计数减一
↓
然后我也等待其他人到达

That is, similar to:

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

However, arrive_and_wait() expresses intent more directly.


2.7.latch is one-shot

This is the most important characteristic of a latch:

只能用一次

For example:

std::latch done(3);

Final:

3 → 2 → 1 → 0

After reaching 0:

不会重新回到 3

Therefore, a latch is particularly suitable for:

等待一批初始化任务完成

等待几个线程启动就绪

等待一次性并行任务全部结束

Instead, it is not suitable for:

第一轮同步
第二轮同步
第三轮同步
...

This scenario should use:

std::barrier

2.8.A latch can be understood as a “one-time door opening”.

For example:

初始 latch(3)

还差 3 人
门关着

还差 2 人
门关着

还差 1 人
门关着

最后 1 人到达
计数变 0

门打开

以后永远保持打开

Therefore:

latch
≈ 一次性会合点

3.barrier: reusable phase synchronization

3.1.barrier: reusable phase synchronization

std::barrierWhat is solved is:

Multiple threads need to synchronize round by round.

For example, there are three threads:

线程 A
线程 B
线程 C

Each thread must execute:

第 1 阶段
↓
等待所有人完成

第 2 阶段
↓
等待所有人完成

第 3 阶段

At this point:

std::barrier

Very suitable.


3.2.barrier can be understood as "must gather every round"

Assumption:

std::barrier sync_point(3);

Indicates:

每一轮都要等 3 个参与者到达

First round:

A 到了
B 到了
C 到了
↓
3 人到齐
↓
全部放行进入下一轮

Then the barrier automatically re-arms:

下一轮继续等 3 人

Therefore:

latch
→ 只集合一次

barrier
→ 每一轮都重新集合

3.3.barrier example

#include <barrier>
#include <print>
#include <thread>
#include <vector>

std::barrier sync_point(3);

void worker(int id)
{
    std::println("phase 1: {}", id);

    sync_point.arrive_and_wait();

    std::println("phase 2: {}", id);

    sync_point.arrive_and_wait();

    std::println("phase 3: {}", id);
}

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();
    }
}

Possible output:

phase 1: 2
phase 1: 0
phase 1: 1

phase 2: 1
phase 2: 2
phase 2: 0

phase 3: 0
phase 3: 2
phase 3: 1

Within each stage:

谁先打印

Uncertain.

However, it must satisfy:

所有 phase 1 都完成
↓
才可能出现 phase 2

所有 phase 2 都完成
↓
才可能出现 phase 3

So it will not happen:

线程 A 已经进入 phase 2

但线程 B 连 phase 1 的 barrier 都还没到

3.4.arrive_and_wait()

Most common operations of barrier:

sync_point.arrive_and_wait();

Indicates:

我已经完成这一阶段
↓
记录一次到达
↓
然后等其他参与者

When all participants in this phase have arrived:

所有等待线程一起进入下一阶段

Therefore:

arrive_and_wait();

Very consistent:

“这一轮我做完了,现在等大家。”

this semantics.


3.5.After the barrier, it automatically proceeds to the next round.

This is the biggest difference between it and a latch.

latch:

计数归零
↓
永久打开
↓
结束

barrier:

本轮所有人到达
↓
本轮完成
↓
自动开始下一轮
↓
重新等待参与者

Therefore:

latch
→ 一次性同步

barrier
→ 循环同步

3.6.completion function

std::barrier You can also set:

Stage completion function

It will be at:

本轮所有参与者都到达

Execute once later.

Conceptually:

所有线程完成第 N 阶段
        ↓
completion function
        ↓
进入第 N+1 阶段

This applies to:

所有线程完成本轮计算
↓
统一更新一次共享状态
↓
所有线程开始下一轮

For example, parallel algorithms may require:

第 1 轮:
每个线程算自己那部分

所有人到 barrier

completion:
交换 / 汇总本轮结果

第 2 轮:
基于新结果继续计算

3.7.The completion function should not do too much heavy work

Because:

所有参与者都在等这个阶段真正结束

So if the completion function:

执行很慢

做长时间 IO

sleep

等待其他锁

执行复杂阻塞操作

Then all participants will be dragged down together.

So the completion function is more suitable for:

很短的状态更新

交换阶段数据

维护轮次

轻量汇总

3.8.arrive_and_drop()

Assume the barrier initially has:

3 个参与者

But after a thread finishes executing the current phase:

I will not participate in the next few rounds.

At this point, you cannot directly:

退出线程

Because of the barrier, I still thought:

下一轮仍然要等 3 人

But in fact, from then on there is only:

2 人

As a result, the other two threads will never be able to wait for the third one.

Therefore, it is necessary to:

barrier.arrive_and_drop();

Indicates:

我完成了本轮到达

并且:

以后每一轮都不用再等我

In other words, permanently subtract the number of future participants.


4.Selection and Comparison

4.1.The most core difference between latch and barrier

You can remember it directly like this:

latch
→ 一次性的

barrier
→ 可重复的

For example:

4.1.1.latch

等 4 个模块初始化完成
↓
程序正式启动

Initialization occurs only once.

Therefore:

std::latch

Very suitable.

4.1.2.barrier

4 个线程一起做第 1 轮
↓
集合

一起做第 2 轮
↓
集合

一起做第 3 轮
↓
集合

Synchronize every round.

Therefore:

std::barrier

is more appropriate.


4.2.How to choose between semaphore, latch, and barrier?

You can directly look at this table:

Requirement Recommended Tool
At most N threads are allowed to acquire the resource simultaneously std::counting_semaphore
A simple 0/1 permit or inter-thread signal std::binary_semaphore
Wait for N participants to all complete at once std::latch
Multiple threads must synchronize each round std::barrier
Protect consistency of shared objects std::mutex
Wait for complex shared conditions to hold std::condition_variable

4.3.and the difference from mutex

mutex What is focused on:

谁能进入临界区

For example:

std::scoped_lock lock(mutex);

Expression:

这段共享数据同时只允许一个线程操作

While semaphore focuses on:

还有多少许可

latch Follow:

还有多少参与者没完成

Barrier focus:

这一轮所有参与者到齐了吗

So these tools:

不是 mutex 的不同写法

Instead:

解决不同同步模式

4.4.Relationship with condition_variable

Theoretically, many synchronizers can be hand-built by oneself.

For example, latch can be roughly used as:

counter

mutex

condition_variable

Implementation:

counter 每完成一个任务减 1

counter == 0
↓
notify_all()

The barrier can also be maintained by yourself:

当前轮次
参与者数量
已到达数量
condition_variable

Implementation.

But this can easily produce:

计数错误

边界条件错误

通知丢失

轮次状态错误

异常退出后人数不一致

Therefore:

condition_variable
→ 通用零件

semaphore / latch / barrier
→ 已经封装好的常见同步模式

If the standard library already has the model you want to express:

优先使用标准抽象

Typically, the code will be made to:

更短

更清楚

更不容易写错

更容易让别人看懂

4.5.Typical scenarios of semaphore

counting_semaphore Commonly seen in:

限制同时下载任务数量

限制数据库连接数

限制 GPU / 加速器任务并发数

资源池可用对象数量

限制同时进行的网络请求

控制同时执行的重任务数量

For example:

线程可以有 100 个

但数据库连接池只有 10 个连接

→ semaphore 初始许可 = 10

This is quite natural.


4.6.Typical scenarios for latch

latch is commonly found in:

多个模块并行初始化

主线程等待所有初始化结束

多个并行任务全部完成后统一继续

启动多个 worker,等待全部 ready

The keywords are:

一次性

4.7.Typical scenarios for barriers

barrier is commonly seen in:

并行数值计算

迭代算法

分块处理

多线程仿真

每一轮都依赖上一轮完整结果

The keywords are:

一轮一轮同步

5.Common Errors

5.1.Error: Treating a semaphore as a mutex

Misunderstanding:

用了 semaphore
→ 共享 vector 自动线程安全

Incorrect.

Semaphore only controls:

许可数量

Consistency of shared objects may still require:

std::mutex

5.2.Error: forgot to release after acquire

For example:

semaphore.acquire();

if (error)
{
    return; // 忘了 release
}

This will permanently lose one license.


5.3.Error: Assuming binary semaphore has mutex ownership rules

Although the binary semaphore only has:

0 / 1

But it still:

不是 mutex

许可可以由不同线程 acquire / release。


5.4.Error: Treating the latch as a tool that can be reset

std::latch:

只能归零一次

After zeroing, it will not:

重新恢复初始计数

When cyclic synchronization is needed, use:

std::barrier

5.5.Error: The number of participants in barrier is incorrect

For example:

std::barrier barrier(4);

But in reality, there is only:

3 个线程

will call:

arrive_and_wait();

Then:

永远差第 4 个参与者

All threads may be stuck permanently.


5.6.Error: the thread exited, but did not arrive_and_drop()

If a barrier participant:

以后不再参加后续轮次

exits directly, then the barrier will still:

在后续每一轮等它

Should:

arrive_and_drop();

5.7.Error: completion function too slow

During execution of the completion function:

所有参与者都在等阶段完成

So it is not suitable for:

长时间 IO

sleep

复杂阻塞

重计算

危险的锁嵌套

6.Final summary

These three C++20 synchronization tools can be remembered as follows:

semaphore
→ 有几张通行证

latch
→ 一次性等大家到齐

barrier
→ 每一轮都等大家到齐

More specifically:

std::counting_semaphore
→ 允许同时存在多个许可

std::binary_semaphore
→ 只有 0 / 1 个许可

std::latch
→ 一次性倒计时,归零后永久打开

std::barrier
→ 每轮所有参与者到达后,自动开始下一轮

Their relationship with mutex:

mutex
→ 保护共享状态

semaphore / latch / barrier
→ 表达更高层的同步模式

If your requirements can be expressed directly using standard synchronization primitives:

Prefer standard tools over hand-written mutex + condition_variable + counter.

Finally, it can be completely distinguished in one sentence:

mutex
→ “谁能进?”

semaphore
→ “还能进几个?”

latch
→ “这一次人到齐了吗?”

barrier
→ “这一轮人到齐了吗?”