多线程内存模型(3):锁,原子变量

多线程内存模型(3):锁,原子变量

锁是多线程编程中最常用的同步机制,用于保护共享资源,防止多个线程同时访问或修改,从而避免数据不一致或竞态条件的发生。以下代码是互斥锁的使用,利用RAII来完成自动锁的自动析构。

1
2
3
4
5
6
7
8
9
int counter = 0;
std::mutex mtx;

void increment(int times) {
for (int i = 0; i < times; i++) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}

increment函数是使用互斥锁保护共享资源的函数,可以在多线程调用时保证线程安全性。锁有两种状态,一种是没有被其他线程获取,此时可以马上获取锁的所有权。另一种是已被其他线程获取,此时当前线程获取就会进入阻塞,由用户态进入内核态,等待满足条件时再次唤醒。

但在increment函数中,++counter操作开销要比用户态和内核态之间上下文切换带来的开销要小得多。为了优化性能,需要使用开销更小的同步方式。

原子变量

原子操作是一种不可分割的操作,即操作要么完全执行,要么完全不执行,中间不会被中断或看到不完整的状态。在多线程编程中,原子操作可以在不使用锁的情况下保证线程安全,防止数据竞争。而具有原子操作方法的变量叫做原子变量。

原子操作并非不使用锁,只是在更底层使用Lock#指令锁,可以以一个更低开销的方式来实现同步,原子操作不需要进入内核态,没有上下文切换,因此有更高的性能。但也因此原子操作并不支持太复杂的操作。

原子变量支持的操作

加载和存储操作

  • load():从原子变量中读取值。
  • store(value):将值存储到原子变量中。

在实际使用中,可以直接赋值和取值,编译器会帮我们调用该方法,而不需要刻意调用loadstore

交换操作

  • exchange(value):将新值存储到原子变量中,并返回旧值。

比较并交换

  • compare_exchange_strong(expected, desired):如果原子变量的当前值等于 expected,则将其设置为 desired,否则不做任何操作。返回是否成功执行替换。
  • compare_exchange_weak(expected, desired):与 compare_exchange_strong 类似,但可能会在并发情况下产生伪失败(即使条件满足,也可能失败),适用于循环重试的情况。

算术操作

  • fetch_add(value):将 value 加到原子变量的当前值上,并返回旧值。
  • fetch_sub(value):将 value 从原子变量的当前值中减去,并返回旧值。

位操作

  • fetch_and(value):对原子变量的当前值与 value 进行按位与操作,并返回旧值。
  • fetch_or(value):对原子变量的当前值与 value 进行按位或操作,并返回旧值。
  • fetch_xor(value):对原子变量的当前值与 value 进行按位异或操作,并返回旧值。

增减操作

  • ++--:原子变量的递增和递减操作。返回新值或旧值,取决于操作是前缀还是后缀形式。

内存序控制

原子变量除了提供原子操作之外,也提供了多种内存序选项,可以用来控制原子变量前后包括非原子变量的内存顺序。atomic头文件提供了6中内存序,用来进行不同的内存序控制。

memory_order_relaxed

不对操作的顺序或可见性施加任何约束。只保证原子操作本身的原子性,而不保证与其他原子操作或非原子操作的顺序。这种用法适用于不依赖操作顺序的场景,通常用于计数器、统计数据等场景。

1
2
3
4
5
std::atomic<int> counter(0);

void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
}

在这个例子中,允许多个线程同时调用increment,只需要确保计数器的递增是原子的,而不关心累加操作的顺序。

memory_order_acquire

保证当前原子操作之前的所有读写操作都能被原子操作之后的操作可见。它是通过保证所有原子操作之后的读写不能重排到原子操作之前来保证可见性的。

memory_order_release

保证当前原子操作之前的所有读写操作都能被原子操作之后的操作可见。但它是通过保证所有原子操作之前的操作不能重排到原子操作之后来保证可见性的。

这两者内存序经常同时使用,例如这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::atomic<int*> ptr(nullptr);
int data;

void producer() {
data = 42;
ptr.store(&data, std::memory_order_release);
}

void consumer() {
int* p;
while (!(p = ptr.load(std::memory_order_acquire)))
;
assert(*p == 42);
}

memory_order_consume

这个与memory_order_acquire功能类似,也是保证原子操作之后的读写不能重排到原子操作之前来保证可见性的,与memory_order_acquire的区别是只保证有依赖关系的读写操作不重排到原子操作之前。但实际上很多编译器都没有正确地实现memory_order_consume,导致等同于memory_order_acquire

memory_order_acq_rel

结合了memory_order_acquirememory_order_release的特性。既保证了获取操作的可见性,又保证了释放操作的顺序性。常用于读-改-写的场景,如比较并交换(CAS)操作中,既需要获取又需要释放,例如这个简单的无锁栈。

1
2
3
4
5
6
7
8
9
std::atomic<Node*> head(nullptr);

void push(Node* new_node) {
Node* old_head = head.load(std::memory_order_relaxed);
do {
new_node->next = old_head;
} while (!head.compare_exchange_weak(old_head, new_node,
std::memory_order_acq_rel));
}

memory_order_seq_cst

全序一致性,是最强的内存序保证。所有线程中的所有原子操作都按顺序执行,避免了所有的重排。它是默认的内存序类型,例如这个多线程生产者-消费者模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::atomic<int> data(0);
std::atomic<bool> flag(false);

void producer() {
data.store(42, std::memory_order_seq_cst);
flag.store(true, std::memory_order_seq_cst);
}

void consumer() {
while (!flag.load(std::memory_order_seq_cst))
; // busy-wait
assert(data.load(std::memory_order_seq_cst) == 42); // 必然成立
}

作者

echo

发布于

2024-09-20

更新于

2024-09-20

许可协议

评论