執行人: PlusThousand0107
專題解說錄影
檢視〈Concurrency Primer〉草稿並記錄閱讀過程中的疑惑,嘗試用 C11 Atomics 改寫原有程式碼,並確保在 x86-64 及 QEMU/Arm 正確運作。預計產出:
檢視〈Concurrency Primer〉草稿並記錄閱讀過程中的疑惑,,參見〈Concurrency Primer〉導讀,確保原本的 C++ 程式都以 C11 Atomics 改寫並驗證。
改寫時,先列出原本的 C++11 程式碼,隨後則是改寫後的 C11 Atomics 實作
原始程式碼 :
這裡無法保證在 v
被設為 42 之後能馬上將 v_ready
設為 true
,可能會有其他執行序在 v_ready
被更改前搶先執行,這就會導致最後的答案錯誤。
以 C11 Atomics 改寫後 :
原本 int
以及 bool
的變數型態在這裡會用 atomic_int
和 atomic_bool
來取代,且存取值的時候不會直接存取,而是使用 atomic_store()
和 atomic_load()
的方式來載入和儲存。
原始程式碼 :
這裡 af
可以決定是否讓程式執行或是等待,如果 af
的上一個值為 false
則表示可以進入並執行,如果是 true
的話則表示目前正被占用,必須等待直到釋放。
以 C11 Atomics 改寫後 :
將 af.test_and_set()
和 af.clear()
改成 atomic_flag_test_and_set()
和 atomic_flag_clear()
。
原始程式碼 :
TakeState
有 3 種狀態,分別是 Idle
、 Running
、 Cancelled
,這裡用 ts
紀錄當前狀態。taskLoop()
為一個迴圈,讓 task 持續執行。cancel()
可以取消正在執行的 task ,當 ts
和 expected
相等時,會更新 ts
為 TaskState::Cancelled
,並回傳 true
,若不相等則不對 ts
做修改且直接回傳 false
。以 C11 Atomics 改寫後 :
CAS 的部分會從原來的 ts.compare_exchange_strong()
改成 atomic_compare_exchange_strong()
。
從課程教材選定 lock-free 程式碼 (例如第 15 週測驗題),以 C11 Atomics 實作並解釋運作原理,需要用 ThreadSanitizer 驗證,作為日後教材的補充內容。
spsc_queue
結構counter_t
來紀錄讀取端 (開始) 以及寫入端 (結束) 的索引,可以幫助判斷當下佇列是否為空 (empty) 或滿 (full)。head
: 主要供 enqueue
使用,會追蹤 producer
的位置,並記錄下一個要寫入的索引。tail
: 主要供 dequeue
使用,會追蹤 consumer
的位置,並記錄下一個要讀取的索引。batch_history
: 負責記錄 batch_size
。start_c
、 stop_c
: 於測試時使用,負責量測操作的執行時間。data[SPSC_QUEUE_SIZE]
: 保存 spsc_queue
的資料,供之後 producer
和 consumer
存取使用。dequeue
函式self->tail.r
和 self->batch_tail
是否相等。tmp_tail
暫存一個可能的位置,但若 tmp_tail
超過或是等於 SPSC_QUEUE_SIZE
的話,則將 tmp_tail
設為 0。此外,如果 self->batch_history
小於 SPSC_BATCH_SIZE
的話,會把 self->batch_history
和 SPSC_BATCH_INCREAMENT
相加,但不能超過 SPSC_BATCH_SIZE
。wait_ticks(SPSC_CONGESTION_PENALTY)
來防止 data race
發生 ( 這裡 SPSC_CONGESTION_PENALTY
等於 1000
),如果 batch_size
等於 0 的話表示佇列為空並直接回傳 SPSC_Q_EMPTY
,否則會將 tmp_tail
設為 self->tail.r + batch_size
繼續找下去。這裡 *val_ptr
會指向最尾端的資料 self->data[self->tail.r]
,接著, self->data[self->tail.r]
會被 SPSC_QUEUE_ELEMENT_ZERO
清空,因為移除了一個元素所以要做 self->tail.w++
。
enqueue
函式self->head.r
和 self->batch_head
是否相等。tmp_head
暫存可能的位置,若超過或等於 SPSC_QUEUE_SIZE
的話,則會被設為 0。self->data[tmp_head]
不為 0 ,就表示裡面已經有存放別的值了,也就是滿了的意思,這時會用 wait_ticks
延遲 SPSC_CONGESTION_PENALTY
的時間,並且回傳 SPSC_Q_FULL
。self->batch_head = tmp_head
,並將值寫入 self->data[self->head.r]
,因為新增了一個所以要用 self->head.w++
。consumer
函式sched_setaffinity()
能讓執行序在指定的 CPU 上執行,第一個參數為 pid
,設為 0 表示當前的 process 。如果最後 sched_setaffinity()
的回傳小於 0 ,則表示配置失敗,會顯示錯誤資訊並終止。
queues[cpu_id]
的 start_c
紀錄當下時間作為開始的時間。TEST_SIZE
次,且如果 dequeue()
的回傳結果不等於 0 就表示目前還不是 SPSC_OK
,所以會被卡住,不會繼續往下執行。queues[cpu_id]
的 stop_c
紀錄結束時間。queues[cpu_id]
的 stop_c
時間減去 start_c
時間並除以 (TEST_SIZE + 1)
來表示操作所花費的平均 cycle 。producer
函式uint64_t
型態的 start_c
紀錄當下時間作為開始的時間。TEST_SIZE + SPSC_BATCH_SIZE
,內層的則會根據傳入的引數 num
來決定要跑幾輪。和 consumer
一樣,當 enqueue()
的回傳結果不等於 0 時也會被卡住不繼續往下執行。uint64_t
型態的 stop_c
紀錄結束時間。編譯 (需加上 -lpthread
) :
輸出 (這裡的 max_threads = 2
) :
從結果上可以看到 producer
和 consumer
所分配到的 CPU ID ,以及執行所花費的平均 cycle。
編譯 (需再加上 -fsanitize=thread
) :
輸出 :
可以看到運行至 producer
和 consumer
時會觸發 data race 而導致程式無法正常結束,所以接下來會以 C11 Atomics 來改寫。
counter_t
和 spsc_queue
結構原先使用 volatile
的部分在這裡都被給成了 atomic_uint
。
dequeue
函式可以發現到本來可以直接比較的 self->tail.r
和 self->batch_tail
在這裡需先經過 atomic_load()
載入後才能進行比較。儲存時也是一樣,必須用 atomic_store()
來存以防存入的結果不同。此外,做計算時也會改用 atomic_fetch_add()
。
enqueue
函式編譯 :
輸出 :
可以看到改成 C11 Atomics 後會犧牲掉一些效能,所以花費的平均 cycle 從原先的 10 增加到了 39。
這樣的解釋不足以反映出你的變更,請善用 uftrace 一類的工具找出具體原因
編譯 (需再加上 -fsanitize=thread
) :
輸出 :
雖然已經加上 atomics 但仍有 data race 的問題,這表示可能還有需加上 atomics 保護的部分沒有被修改到。
研讀以下材料:
紀錄認知和疑惑,並回答以下:
compiler barrier 是 memory barrier 的一種 (另一種為 CPU barrier) ,用於控制編譯器對程式碼進行的優化,像是 volatile
。
volatile
會告訴編譯器定義的變數可能會隨時改變,所以編譯完後程式若想要存取這個變數就要直接去這個變數的記憶體位址讀取編譯器最佳化可能會影響到讀取記憶體的順序,像是指令重排列 (instruction reordering) 。
compiler barrier
來防止重排。例子
volatile
這裡使用 $ gcc -S -O2 -masm=intel test_without_volatile.c
來編譯,產生出的組合語言如下。
volatile
這裡也是用 $ gcc -S -O2 -masm=intel test_with_volatile.c
來編譯並產生組合語言。
可以發現到有加 volatile
的組合語言會用 mov DWORD PTR
的方式來存取資料,所以轉換後的組合語言明顯比沒加 volatile
的要更長一點,這主要和少了重排列的優化有關,但這樣所帶來的好處就是保障了變數資料的正確性,不會因為執行序順序上的不同而有錯誤的結果。
memory barrier 能維持對記憶體存取時的順序,避免優化而導致指令重排的問題。
memory barrier 所需考量因素 :