Skip to content

Commit b70f15e

Browse files
committed
完成第五章 std::atomic_flag 这一节的内容
1 parent 3367e2d commit b70f15e

File tree

1 file changed

+80
-0
lines changed

1 file changed

+80
-0
lines changed

md/05内存模型与原子操作.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,83 @@ struct trivial_type {
215215
2. **Load 操作(加载操作)**:可选的内存序包括 `memory_order_relaxed``memory_order_consume``memory_order_acquire``memory_order_seq_cst`
216216

217217
3. **Read-modify-write(读-改-写)操作**:可选的内存序包括 `memory_order_relaxed``memory_order_consume``memory_order_acquire``memory_order_release``memory_order_acq_rel``memory_order_seq_cst`
218+
219+
## `st::atomic_flag`
220+
221+
`std::atomic_flag` 是最简单的原子类型,这个类型的对象可以在两个状态间切换:**设置(true)****清除(false)**。它很简单,通常只是用作构建一些库设施,不会单独使用或直接面向普通开发者。
222+
223+
在 C++20 之前,`std::atomic_flag` 类型的对象需要以 [`ATOMIC_FLAG_INIT`](https://zh.cppreference.com/w/cpp/atomic/ATOMIC_FLAG_INIT) 初始化,可以确保此时对象处于
224+
"清除"(false)状态。
225+
226+
```cpp
227+
std::atomic_flag f = ATOMIC_FLAG_INIT;
228+
```
229+
230+
`C++20``std::atomic_flag` 的默认[构造函数](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/atomic_flag)保证对象为“清除”(false)状态,就不再需要使用 `ATOMIC_FLAG_INIT`
231+
232+
`ATOMIC_FLAG_INIT` 其实并不是什么复杂的东西,它在不同的标准库实现中只是简单的初始化:在 [`MSVC STL`](https://github.com/microsoft/STL/blob/daeb0a6/stl/inc/atomic#L2807-L2808) 它只是一个 `{}`,在 [`libstdc++`](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/atomic_base.h)[`libc++`](https://github.com/llvm/llvm-project/blob/00e80fb/clang/lib/Headers/stdatomic.h#L169) 它只是一个 `{ 0 }`。也就是说我们可以这样初始化:
233+
234+
```cpp
235+
std::atomic_flag f ATOMIC_FLAG_INIT;
236+
std::atomic_flag f2 = {};
237+
std::atomic_flag f3{};
238+
std::atomic_flag f4{ 0 };
239+
```
240+
241+
使用 ATOMIC_FLAG_INIT 宏只是为了统一,我们知道即可。
242+
243+
当标志对象已初始化,它只能做三件事情:**销毁、清除、设置**。这些操作对应的函数分别是:
244+
245+
1. **`clear()`** (清除):将标志对象的状态原子地更改为清除(false)
246+
2. **`test_and_set`**(测试并设置):将标志对象的状态原子地更改为设置(true),并返回它先前保有的值。
247+
3. **销毁**:对象的生命周期结束时,自动调用析构函数进行销毁操作。
248+
249+
每个操作都可以指定内存顺序。`clear()` 是一个“读-改-写”操作,可以应用任何内存顺序。默认的内存顺序是 `memory_order_seq_cst`。例如:
250+
251+
```cpp
252+
f.clear(std::memory_order_release);
253+
bool r = f.test_and_set();
254+
```
255+
256+
1.`f` 的状态原子地更改为清除(false),指明 `memory_order_release` 内存序。
257+
258+
2.`f` 的状态原子地更改为设置(true),并返回它先前保有的值给 `r`。使用默认的 `memory_order_seq_cst` 内存序。
259+
260+
> 不用着急,这里还不是详细展开聊内存序的时候。
261+
262+
`std::atomic_flag` [不可复制](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/atomic_flag)不可移动[不可赋值](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/operator%3D)。这不是 `std::atomic_flag` 特有的,而是所有原子类型共有的属性。原子类型的所有操作都是原子的,而赋值和拷贝涉及两个对象,破坏了操作的原子性。拷贝构造和拷贝赋值会先读取第一个对象的值,然后再写入另一个对象。对于两个独立的对象,这里实际上有两个独立的操作,合并这两个操作无法保证其原子性。因此,这些操作是不被允许的。
263+
264+
有限的特性使得 `std::atomic_flag` 非常适合用作制作**自旋锁**
265+
266+
```cpp
267+
class spinlock_mutex {
268+
std::atomic_flag flag{};
269+
public:
270+
void lock() {
271+
while (flag.test_and_set(std::memory_order_acquire));
272+
}
273+
274+
void unlock() {
275+
flag.clear(std::memory_order_release);
276+
}
277+
};
278+
```
279+
280+
我们可以简单的使用测试一下,它是有效的:
281+
282+
```cpp
283+
spinlock_mutex m;
284+
285+
void f(){
286+
std::lock_guard<spinlock_mutex> lc{ m };
287+
std::cout << "😅😅" << "❤️❤️\n";
288+
}
289+
```
290+
291+
> [运行](https://godbolt.org/z/9rs9sxsns)测试。
292+
293+
稍微聊一下原理,我们的 `spinlock_mutex` 对象中存储的 `flag` 对象在默认构造时是清除 (`false`) 状态。在 `lock()` 函数中调用 `test_and_set` 函数,它是原子的,只有一个线程能成功调用并将 `flag` 的状态原子地更改为设置 (`true`),并返回它先前的值 (`false`)。此时,该线程成功获取了锁,退出循环。
294+
295+
`flag` 对象的状态为设置 (`true`) 时,其线程调用 `test_and_set` 函数会返回 `true`,导致它们继续在循环中自旋,无法退出。直到先前持有锁的线程调用 `unlock()` 函数,将 `flag` 对象的状态原子地更改为清除 (`false`) 状态。此时,等待的线程中会有一个线程成功调用 `test_and_set` 返回 `false`,然后退出循环,成功获取锁。
296+
297+
`std::atomic_flag` 的局限性太强,甚至不能当普通的 bool 标志那样使用。一般最好使用 `std::atomic<bool>`,下节,我们来使用它。

0 commit comments

Comments
 (0)