大家。我写了一个演示来重现 cppreference 中引用的问题。
cpp参考演示 我发现一些文档和博客说这可能不会在 x86 芯片上重现,但在 ARM 芯片上重现,因为 ARM arch 是弱内存顺序。因此,我专门在Apple M1芯片(ARM)上进行了实验。但是,它也不能重现 r1 == r2 == 42。
如果我的演示有任何问题,或者您有任何其他演示可能会重现内存顺序问题。
#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <ostream>
#include <queue>
#include <thread>
#define CHECK(condition) \
if (!(condition)) { \
std::cerr << "Check failed: " << #condition << std::endl; \
std::abort(); \
}
class TaskQueue {
public:
TaskQueue() = default;
~TaskQueue() = default;
void AddTask(const std::function<void()> &task) {
std::lock_guard<std::mutex> lock(mtx_);
tasks_.push(task);
}
void AddTaskWithCallBack(std::function<void()> task,
std::function<void()> callback) {
std::lock_guard<std::mutex> lock(mtx_);
tasks_.push([task, callback]() {
task();
callback();
});
}
bool Empty() const {
std::lock_guard<std::mutex> lock(mtx_);
return tasks_.empty();
}
bool GetTask(std::function<void()> &gotten_task) {
std::lock_guard<std::mutex> lock(mtx_);
if (tasks_.empty()) {
return false;
}
gotten_task = tasks_.front();
tasks_.pop();
return true;
}
private:
std::queue<std::function<void()>> tasks_;
mutable std::mutex mtx_;
};
TaskQueue thread1_queue;
TaskQueue thread2_queue;
void ThreadWork(std::chrono::milliseconds delay, TaskQueue &queue) {
std::cout << "Launched thread : " << std::this_thread::get_id() << std::endl;
std::chrono::steady_clock::time_point start =
std::chrono::steady_clock::now();
auto end = start + delay;
std::function<void()> task;
while (std::chrono::steady_clock::now() < end) {
if (queue.GetTask(task)) {
CHECK(task);
task();
} else {
std::this_thread::yield();
}
}
}
std::atomic<int> x(-1);
std::atomic<int> y(-2);
int r1 = -3;
int r2 = -4;
std::mutex mtx;
int main() {
std::cout << "hello world" << std::endl;
std::thread t2(ThreadWork, std::chrono::milliseconds(20000),
std::ref(thread2_queue));
std::thread t1(ThreadWork, std::chrono::milliseconds(20000),
std::ref(thread1_queue));
for (int i = 0; i < 100; ++i) {
x.store(-1);
y.store(-2);
r1 = -3;
r2 = -4;
thread1_queue.AddTask([]() {
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
});
thread2_queue.AddTask([]() {
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
});
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "r1: " << r1 << ", r2: " << r2 << std::endl;
}
t1.join();
t2.join();
}
为了避免线程调度的影响,我构建了两个线程并让它们始终工作,然后将实验代码发布到任务队列中。
cppreference 示例的目的是演示一个不太可能且自相矛盾的场景。 在实践中您观察不到这种行为并不罕见。
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
由于所有加载和存储都被放松,因此将允许 D 在 C 之前有效发生,并且如果线程 2 首先运行,则允许在 A 之前发生。
特别是,如果 D 在线程 2 中的 C 之前完成,则可能会发生这种情况,无论是由于编译器重新排序还是在运行时。
但是,编译器没有特别的动机来重新排序 C 和 D。 armv8-clang 输出如下:
ldr w8, [x8, :lo12:x]
mov w11, #42
str w8, [x9, :lo12:r2]
str w11, [x10, :lo12:y]
由于硬件原因,D 仍然有可能发生在 C 之前,因为这里不存在内存障碍。 但是,线程 1 和线程 2 执行的工作很少,因此它们很可能只是在另一个线程运行之前很久就按顺序运行这几条指令。
因此,如果你确实观察到
r1 == r2 == 42
,那将是一个巨大的巧合,尽管这并非不可能。