之前一段時間偶然在 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