之前一段時間偶然在 B 站上刷到了南京大學蔣炎巖(jyy)老師在直播作業系統網課,點進直播間看了一下發現這個老師實力非凡,上課從不照本宣科,而且旁征博引又不吝于親自動手演示,于是點了關注,后來開始看其網課錄播,其中一節的標題吸引了我,多處理器編程:從入門到放棄 (執行緒庫;現代處理器和寬松記憶體模型),“多處理器編程”這個詞讓我聯想到去年看的《The Art of Multiprocessor Programming》,于是仔細看了一下這節網課,里面介紹到了一個試例 write_x_read_y,它是用 C 語言和行內匯撰寫的,它用來說明運行期指令重排,這個試例能夠成功觀測到運行期指令重排現象,這讓我不得不佩服 jyy 的實踐精神,之前看了一些介紹 C++ 記憶體模型的文章,沒有一個能用可復現的完整代碼說明問題的,全部都是說這段代碼可能出現 xx 結果,沒有實際的執行結果,在 C++ 記憶體模型中,這個測驗用例除了能夠說明運行期指令重排,也能用于說明 happens-before consistency 和 sequential consistency 的差別,于是嘗試用 C++ Atomic 來實作這段代碼,看看能不能觀測到預期結果,
首先執行緒庫 pthread 替換為 std::thread,行內匯編替換為 std::atomic,且 load 和 store 操作全部使用最弱的 std::memory_order_relaxed 記憶體序,完整的代碼如下:
// write_x_read_y.cpp
#include <atomic>
#include <thread>
#include <stdio.h>
static std::atomic_int flag{0};
inline void wait_flag(int id)
{
while (!(flag & (0x1 << id))) {}
}
inline void clear_flag(int id)
{
flag.fetch_and(~(0x1 << id));
}
std::atomic_int x{0}, y{0};
void write_x_read_y()
{
while (true) {
wait_flag(0);
x.store(1, std::memory_order_relaxed); // t1.1
int v = y.load(std::memory_order_relaxed); // t1.2
printf("%d ", v);
clear_flag(0);
}
}
void write_y_read_x()
{
while (true) {
wait_flag(1);
y.store(1, std::memory_order_relaxed); // t2.1
int v = x.load(std::memory_order_relaxed); // t2.2
printf("%d ", v);
clear_flag(1);
}
}
int main()
{
std::thread t1(write_x_read_y), t2(write_y_read_x);
while (true) {
x = 0, y = 0;
flag = 0b11;
while (flag) {}
printf("\n");
fflush(stdout);
}
t1.join();
t2.join();
}
注意這段代碼要開啟代碼優化才能觀測到運行期指令重排,這里選擇 O2
g++ -o write_x_read_y.out -O2 -pthread -std=c++11 -Wall -Wextra write_x_read_y.cpp
然后使用 jyy 視頻里使用的 Unix 命令進行測驗并整理結果
./write_x_read_y.out | head -n1000000 | sort | uniq -c
以下結果是在虛擬機環境中執行得到的,宿主機 CPU 型號為 AMD Ryzen 7 5800X,OS 為 Windows 10 x64,虛擬機是 Rocky Linux 8.6,
948739 0 0
50150 0 1
1109 1 0
2 1 1
成功觀測到“0 0”,假設程式按照簡單交叉執行,執行結果只可能是“0 1”、“1 0”、“1 1”這三種,不可能出現“0 0”,也就是說發生了運行期指令重排,
接下來,將 std::memory_order_relaxed 替換為 std::memory_order_release 和 std::memory_order_acquire,再測一遍
x.store(1, std::memory_order_release); // t1.1
int v = y.load(std::memory_order_acquire); // t1.2
printf("%d ", v);
y.store(1, std::memory_order_release); // t2.1
int v = x.load(std::memory_order_acquire); // t2.2
printf("%d ", v);
測驗結果為:
613684 0 0
360557 0 1
25757 1 0
2 1 1
又出現了“0 0”,也就說明這個試例無法區分 relaxed memory model 和 happens-before consistency,這也與理論相符,雖然 t1.1 happens-before t2.2、t2.1 happens-before t1.2,但是卻無法借此推匯出約束關系來限制執行結果,“0 0”依然有可能出現,
接下來替換為 std::memory_order_seq_cst
x.store(1, std::memory_order_seq_cst); // t1.1
int v = y.load(std::memory_order_seq_cst); // t1.2
printf("%d ", v);
y.store(1, std::memory_order_seq_cst); // t2.1
int v = x.load(std::memory_order_seq_cst); // t2.2
printf("%d ", v);
測驗結果為:
132394 0 1
151 1 0
867455 1 1
這次“0 0”并沒有出現,運行期指令重排沒有被觀測到,這與理論相符,使用 std::memory_order_seq_cst 的所有原子操作可以視為簡單交叉執行,也就是 sequential consistency,“0 0”不可能出現,
write_x_read_y 這個試例很好地說明了 C++ 記憶體模型中的 happens-before consistency 和 sequential consistency 的區別,它的代碼片段常見于各種相關文章中,卻沒有完整的代碼和實際的測驗結果,這下也算補全了 C++ 記憶體模型知識的一塊拼圖,
本文來自博客園,作者:mkckr0,轉載請注明原文鏈接:https://www.cnblogs.com/mkckr0/p/16533221.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/500573.html
標籤:C++
上一篇:錯排問題詳解
下一篇:一、HELLO,C++
