執行人: csm1735
專題解說錄影
回顧〈並行和多執行緒程式設計〉教材,以課程測驗題給定的並行程式碼為基礎,著手改進,並確保以 C11 Atomics 撰寫出正確且有效的程式碼,所有程式碼應當通過 Thread Sanitizer 的執行時期檢測。
重做第 8 週測驗題的測驗一,包含延伸問題。
先定義了 maybe_unused
,其作用為針對可能不會使用到的部份,消除編譯時的警告。
宣告了一個名為 refcnt_t
的 struct,可看到其中除了 data
外,還有一個 atomic_uint 的變數 refcount
,用來以 atomic 的型態儲存 reference counting ,而如果程式中有定義 REFCNT_CHECK
, struct 中還會再多一個 magic
的變數,用途為檢查傳入 refcnt_ref
和 refcnt_unref
的 reference 是否由 refcnt_malloc
或 refcnt_strdup
所建立。
先透過 malloc 配置大小為 sizeof(refcnt_t) + len
的記憶體空間,如果配置失敗則直接回傳 NULL ,如果有定義 REFCNT_CHECK
的話,則將 ref->magic
設為事先定義好的魔術數 REFCNT_MAGIC
,接著再透過 atomic_init 將代表 reference counting 的 ref->refcount
初始化為 1,最後會回傳 ref->data
。
使用 offsetof 計算 refcnt_t
的成員 data
距離結構起始位址的偏移量,以透過 (ptr - offsetof(refcnt_t, data))
取得結構起始位址,如果有定義 REFCNT_CHECK
的話,則會去檢查 ref->magic
,接著透過 realloc 重新調整 ref
所指向的記憶體空間的大小為 sizeof(refcnt_t) + len
,如果配置失敗則回傳 NULL ,最後會回傳 ref->data
。
使用 offsetof 計算 refcnt_t
的成員 data
距離結構起始位址的偏移量,以透過 (ptr - offsetof(refcnt_t, data))
取得結構起始位址,如果有定義 REFCNT_CHECK
的話,則會去檢查 ref->magic
,接著再透過 atomic_fetch_add 去將 ref->refcount
做加一的動作,最後會回傳 ref->data
。
使用 offsetof 計算 refcnt_t
的成員 data
距離結構起始位址的偏移量,以透過 (ptr - offsetof(refcnt_t, data))
取得結構起始位址,如果有定義 REFCNT_CHECK
的話,則會去檢查 ref->magic
,接著再透過 atomic_fetch_sub 去將 ref->refcount
做減一的動作,如果 ref->refcount
的值在減一之前為 1 ,即做完減一的動作後會變成 0 的話,則將 ref
做 free 的動作。
先透過 malloc 配置大小為 sizeof(refcnt_t) + strlen(str) + 1
的記憶體空間,如果配置失敗則直接回傳 NULL ,如果有定義 REFCNT_CHECK
的話,則將 ref->magic
設為事先定義好的魔術數 REFCNT_MAGIC
,接著再透過 atomic_init 將代表 reference counting 的 ref->refcount
初始化為 1,再使用 strcpy 將字串 str
複製到 ref->data
,最後會回傳 ref->data
。
main
中使用 pthread_create
來建立 N_THREADS
個執行緒,而新建立的執行緒會去調用 test_thread
來開始執行,refcnt_ref(str)
則是作為 test_thread
的參數傳遞。
在 test_thread
中可發現每次做 fprintf 前都會做 refcnt_ref
的動作,在fprintf 完後再做 refcnt_unref
的動作,而在 fprintf 中可發現有使用到 pthread_self()
,其功能為取得執行緒自身的 ID 。
此外, main
中也使用了 pthread_join
,其作用為去等待指定的執行緒執行完畢,如果沒有去使用 pthread_join
可能會造成我們所建立的執行緒沒有執行完的問題。
在取得 Linux 核心原始程式碼後,可用以下命令去搜尋包含關鍵字 reference count 的 commit。
可發現在 commit ddaf098 提及 reference count ,而相關的程式碼可以在 drivers/base/class.c 及 include/linux/device/class.h 裡面查看。
根據註解及程式碼推測,subsys_get
的功能與 reference count 的 INCREMENT 有關,而 subsys_put
的功能則與 reference count 的 DECREMENT 有關。
在 lxr 搜尋 subsys_get
在 subsys_get
中有呼叫 kset_get
在 kset_get
中有呼叫 kobject_get
在 kobject_get
中有呼叫 kref_get
在 kref_get
中會呼叫 refcount_inc
去做 reference count 的 INCREMENT。
由此可見,subsys_get
確實與增加 reference count 有所關聯。
接著搜尋 subsys_put
在 subsys_put
中呼叫了 kset_put
在 kset_put
中呼叫了 kobject_put
在 kobject_put
中呼叫了 kref_put
kref_put
中會去做 reference count 的 DECREMENT,而當最後的 reference 被釋放之後,該物件也會被釋放。
由此可見,subsys_put
也確實與減少 reference count 有所關聯。
在 drivers/base/class.c 中 class_to_subsys
的功能是將 struct class
轉換為 struct subsys_private
,做這樣的轉換是因為 driver core 內部是需要處理 struct subsys_private
,不是外部的 struct class
,而在找到匹配的 class 後就會 return 與該 class 相關的內部結構 subsys_private,如果沒有找到匹配的 class 則會 return NULL,而在不為 NULL 的時候,就會呼叫 subsys_get
去做 reference count 的 INCREMENT,因此在使用完畢後必須呼叫 subsys_put()
才能正確地去做釋放的動作。
而 commit ddaf098 所修正的問題就是當呼叫 class_dev_iter_init
函式去初始化 class_dev_iter
的時候使用了 class_to_subsys
去取得 subsys_private
結構並使其 reference count 增加,但當 class_dev_iter
使用完畢後,卻沒有將 subsys_private
的 reference count 做減少的動作,由於這個缺失,漸漸地就會造成 memory leak 的問題。
參閱 RCU 同步機制中 RCU 與 reference counting 的對比如下
Reference Counting | RCU | |
---|---|---|
Unreclaimed objects | Bounded | Unbounded |
Contention among readers | High | None |
Traversal forward progress | lock-free | wait-free |
Reclamation forward progress | lock-free | blocking |
Traversal speed | atomic | no-overhead |
Reference acquisition | unconditional | unconditional |
Automatic reclamation | yes | no |
Purpose of domains | N/A | isolate slow reader |
首先,reference counting 的主要問題是 atomic。多個執行緒可能同時增加或減少 reference counting ,這就需要使用 atomic operation 來確保計數的一致性。然而,atomic operation 是相對高成本的操作,特別是當多個執行緒同時競爭一個資源時。在高度並行的情況下,可能會導致性能下降。
再來,reference counting 可能會在多個 readers 的情況下導致高度競爭,如果多個執行緒同時增加或減少引用計數,可能會產生競爭,導致計數結果不正確,而為了解決這個問題,需要使用其他同步機制,但也會使得整體的複雜性增加。
另外,當 reference counting 存在 reference cycles (an object which refers directly or indirectly to itself) 的情況時,會導致計數保持非零,無法正確釋放資源。這種情況需要特殊的機制來檢測和處理,以確保資源能夠正確地釋放,但也會增加 reference counting 的成本和複雜性。
基於以上這些性能、競爭、和資源釋放等方面的考慮,這也是為什麼 Linux 核心的同步機制不依賴 reference counting。
重做第 8 週測驗題的測驗三,包含延伸問題。
在 lfq.h
中,先定義二個結構體。
struct lfq_node
是 lock-free queue node 的結構,其中除了資料以及代表能否 free 的一個 bool 以外,還嵌入了 union 的型別去儲存 next
及 free_next
,next
是 queue 中的下一個 node,free_next
則是 free pool 中的下一個 node。
struct lfq_ctx
是 lock-free queue handler 的結構,儲存了 queue 及 free pool 的頭尾,以及 tid 的 map,還有一個 bool 用來確認是否正在做 free 的動作,此外,也儲存了 hazard pointers 和他的最大長度。
lfq_enqueue
的功能為 push 資料進入 queue 中,作法是將 queue 的尾端替換成 insert_node
,再將原先舊尾端 old_tail
的 next
指向 insert_node
lfq_dequeue
的功能為將資料從 queue 中 pop 出來,首先,會先透過 alloc_tid
取得 tid ,如果順利取得非 -1 的值,就會透過 lfq_dequeue_tid
去做 pop,在 lfq_dequeue_tid
中,會先將要 pop 的 old_head
存入 ctx->HP[tid]
之中,然後去將 queue 的 head
替換成 new_head
也就是 old_head->next
,完成後再將 ctx->HP[tid]
重設回 0,再對 old_head
做 safe_free
。
如果這個 node 能被 free 且不在 hazard pointers 內,就嘗試去做 free 的動作,否則就透過 insert_pool
只將 node 加入 free pool,而如果嘗試去做 CAS(&ctx->is_freeing, 0, 1)
成功的話,就能確實地去將 node 給 free 掉,但如果失敗了就一樣只將 node 加入 free pool,最後再呼叫 free_pool
去嘗試 free 掉 free pool 內的 node。
題目所給的 atomics.h
中所定義的內容主要針對 x86(-64),故這邊用 C11 Atomics 將以 __sync 開頭的內建函式做改寫,為此需要先 #include <stdatomic.h>
將原先的 __sync_sub_and_fetch
改寫成 atomic_fetch_sub
,然而原先的 __sync_sub_and_fetch
所回傳的是做完減法後的新值,而 atomic_fetch_sub
所回傳的則是減法之前的舊值,但查看程式中對 ATOMIC_SUB
的使用,沒有去對回傳值做存取的部份,故沒有進一步修改
將原先的 __sync_add_and_fetch
改寫成 atomic_fetch_add
,然而原先的 __sync_add_and_fetch
所回傳的是做完加法後的新值,而 atomic_fetch_add
所回傳的則是加法之前的舊值,但查看程式中對 ATOMIC_ADD
的使用,只有在 test.c
中的
去對回傳值做存取的動作,雖可針對該行做修改,但原先的版本會取得加法完的新值,也就是使 tid
從 1 開始,而新的版本能使 tid
取得舊值,也就是從 0 開始,故此處也不做修改
將原先的 __sync_bool_compare_and_swap
改寫成 atomic_compare_exchange_strong
,以達到先比較再去修改值的效果,如果成功會 return true ,反之則 return false
將原先的 __sync_lock_test_and_set
改寫成 atomic_exchange
,以達到修改值的效果,而最後都會回傳修改前的值
ATOMIC_SET
跟 ATOMIC_RELEASE
在原先程式中都沒有使用到,而其函式功能為 lock 的相關操作,故此處藉由 atomic_flag_test_and_set
跟 atomic_flag_clear
去對 atomic_flag
的物件做操作,以達到預期的效果
將原先的 __sync_synchronize
改寫成 atomic_thread_fence
,memory order 設為 memory_order_seq_cst
原先的程式碼在經由 Thread Sanitizer 檢測的時候,會發生多起 data race 的問題,以下是其中一筆
因此,根據 Thread Sanitizer 所提供的訊息,針對 data race 的部份,使用 atomic operation 改寫程式碼,以通過 Thread Sanitizer 的檢測。
以上面的訊息為例,去對 test.c
第 72 行的 cnt_producer
的讀取做改寫
相關程式碼修改可見 commit 695aff6
已提交 pull request #18 取代原先的 lf-queue
,所提交的版本以 Ruslan Nikolaev 的論文 A Scalable, Portable, and Memory-Efficient Lock-Free FIFO Queue 所提及的 SCQ (Scalable Circular Queue) 來實作,並採用基於 hazard pointer 的記憶體物件回收機制,程式碼使用 C11 Atomics 撰寫,並通過 Thread Sanitizer 的驗證。
重做第 9 週測驗題的測驗一,包含延伸問題。