contributed by < ambersun1234
>
由於 Bailey–Borwein–Plouffe formula 可以在不需要知道前 位的資訊去計算第 位的數值,所以可以採用並行的方式計算
以這個實作來說,相比頻繁的建立執行緒,我們採用 thread pool 的方式管理,意即 thread 的數量是固定的,可以讓完成計算的 thread 取得下一個工作,如此一來可以大幅度的減少 thread 的建立與刪除
tpool_create
建立一個 thread pool 裡面包含了 4 的 thread,並且共享 jobqueue,當建立 thread 的過程中出錯,會立即中止所有任務,根據 man pthread_create
On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
而 tpool_apply
將每個工作分配到 thread pool 中
新增 new_head
以及 future
將其加入到 threadtask_t 的 singly linked list 中,並且多加了一些錯誤處理機制。需要注意到 pthread_cond_broadcast
,根據 pthread_cond_broadcast(3)
The pthread_cond_broadcast() function is used whenever the shared-variable state has been changed in a way that more than one thread can proceed with its task
因為在這個情況下,4 個 thread 共享 jobqueue,而因為 jobqueue->head
以及 jobqueue->tail
這兩個共享變數被變更,因此要通知所有 worker
執行運算的統一都是 bbp 這個函數,但要注意的是,實作中並不是直接呼叫 bbp,因為有 jobqueue 的存在,因此需要 jobqueue_fetch
的幫忙
這個實作會一直查詢 jobqueue 的 tail,將 tail 的工作取出並且執行,將其執行結果放置於相對應的 future 然後繼續等待直到 jobqueue 為空
運算完成的資料會儲存在各別的 struct tpool_future
裡面,並由 tpool_future_get
取得資料
seconds 用於表示 timeout 的時間(其中 0 seconds 表 blocking wait),首先 while 迴圈檢查是否運算完畢,並且在內部分為 blocking wait 或者 non-blocking wait
根據 pthread_cond_timedwait(3)
The pthread_cond_timedwait() function shall be equivalent to pthread_cond_wait(), except that an error is returned if the absolute time specified by abstime passes (that is, system time equals or exceeds abstime) before the condition cond is signaled or broadcasted, or if the absolute time specified by abstime has already been passed at the time of the call.
所以實作中採用 當前時間+seconds
作為參數傳遞至 pthread_cond_timedwait
該實作十分的精簡,他可以運作在 windows、linux 以及 macOS 上面。首先映入眼簾的是
__sync_fetch_and_add 是 gcc 的 atomic extension
你可能會好奇,單純的 +1, -1 為何會需要 atomic operation,事實上,這些操作涉及到不單單只有一個指令,有可能會包含
如果說這些順序亂掉,資料也有很大的機率跟著亂掉,幸好 gcc 提供的這些 extension 保證不會將指令順序調換,參見以下節錄
In most cases, these builtins are considered a full barrier. That is, no memory operand will be moved across the operation, either forward or backward. Further, instructions will be issued as necessary to prevent the processor from speculating loads across the operation and from queuing stores after the operation.
不過,僅僅擁有這些不足以讓整個 threadpool 成為 lock-free
考慮 thread 執行程式碼
可以發現到基本上它是一個無窮迴圈,裡面持續向 job queue(lfqueue) 申請工作,當取得工作時,就會執行
相比 quiz4 的實作,atomic threadpool
並沒有使用 mutex
這種 read-write lock,這是因為得益於 lfqueue 是為 lock-free 實作,因為 lfqueue_deq
以及 lfqueue_enq
這些操作都是 atomic 的
這裡的 atomic 表示的是,在運行中,並不會有其他部分的系統存取,因此可以被視為 atomic 操作(詳細解釋可參考: How does a function becomes atomic?),考慮 lfqueue_enq
實作
__sync_synchronize(__LFQ_SYNC_MEMORY) 他是一個 memory barrier,其保證嚴格的執行順序,且剩下的操作都是 atomic operation
__sync_bool_compare_and_swap(__LFQ_BOOL_COMPARE_AND_SWAP)
These builtins perform an atomic compare and swap. That is, if the current value of *ptr is oldval, then write newval into *ptr. The “bool” version returns true if the comparison is successful and newval was written. The “val” version returns the contents of *ptr before the operation.
回到正題,所以既然 lfqueue_deq
為 atomic operation,那這樣也就可以保證以下為 lock-free?
因為 lfqueue_deq
操作為 atomic,亦即同一時間只會有一個 thread 嘗試存取 job queue 了對吧,如果有一個 thread 被卡在這個迴圈,那麼是否代表至少有一個 thread 成功取得 task
可參考 Lock-free-程式設計議題
綜合以上討論,我們知道,要達到 lock-free 需要以下
所以首先將 _Atomic
引入
將 tpool_apply
改寫成以下
除了以上實作,在實作中我也加入了避免 ABA problem 的手段,解法通常為加入 版本紀錄
在 java 的實作當中,提供 AtomicsStampedReference,也就是增加版本紀錄的欄位,同理可以應用到這上面
linux2021