contributed by < Hao-yu-lin
>
測驗解答
AAAA:MUTEX_SLEEPING
BBBB:1
CCCC:1
DDDD:1
EEEE:futex_wake
FFFF:futex_wake
reference:Basics of Futexes
futex("Fast userspace mutex") 為 userspace code 提供一種執行緒之間的同步(synchronize)同時伴隨最小程度的 linux kernel 參與。
再引入 futexes 前,使用 syscall 來鎖定共享資源(ex:semop),隨著 programs 的 concurrent 需求增加,lock 行為在整體的執行時間佔了很大一部分,然而 lock 並沒有做任何實際的工作,它只有保證共享資源是安全的。在執行緒間很少競爭共享資源(low contention rate)的情況下,沒有其他執行緒在 wait queue 中等待 lock,但還是要從使用者層級切換到核心模式去檢查,這樣頻繁的 CPU 模式切換會對效能有負面影響,futex 是該問題解法之一。
在大部分的情形下,locks 不存在競爭關係的,如果一個執行緒遇到一個 free lock,代表在同一時間沒有其他的執行緒嘗試鎖定,可以在不使用 syscall 下調用完成,嘗試使用執行成本更低的 atomic operatoins。
然而,當另一個執行緒在同一個時間點,嘗試獲取 lock ,atomic operations 可能會失敗,此時有兩種選項。
futex:wait queue(kernl space) + atomic integer(user sapce),透過 atomic integer,可以知道是否有執行緒在 wait queue 中等待。
FUTEX_WAIT
和 FUTEX_WAKE
,來喚醒其他執行緒或是到 wait queue 等待。futex 的操作幾乎都在 userspace 完成,只有當操作結果不一致需要仲裁時,才會需要進入 kernel space 執行。這讓以 futex 為基礎的 lock 可以高效進行。
futex 在核心中藉由特製的佇列來管理執行緒或行程,可要求某個行程/執行緒 suspend 直到某個條件成立,或 signal 某個條件,來喚醒行程/執行緒。
uint32_t *uaddr
:futex 中使用者層級 atomic integer 所存放的地址int futex_op
:futex operator
FUTEX_WAIT
FUTEX_WAKE
uint32_t val
:
FUTEX_WAIT
代表預期使用者層級 atomic integer 的值FUTEX_WAKE
代表喚醒的執行緒數量main
function 設定共享內存的相關設定後,建立一個 child process,行為如下:
執行結果
使用 futex 等待 futex_addr 有 val,只有當 futex_addr 有值時,會去檢查 futex_addr 的值是否與 val 相等,如果相等 return,否則就 abort()
一個用於喚醒的 wrapper,只有被喚醒時,才會返回。
futex.h
檔案中有三種的 futex 操作,futex_wait
、futex_wake
、futex_requeue
使用 syscall 機制存取 Linux Kernel 內的 futex。
在 /kernel/futex/syscalls.c 會根據不同的 FUTEX_[xxx]_PRIVATE
透過 do_futex
執行對應的 futex。
在 futex.h 中的 futex_wait 實作部分,透過 syscall 方式呼叫 Linux Kernel 中的 futex_wait
此為 Linux Kernel 中的 futex_wait
實作,透過 futex_wait_setup
檢查 uaddr 中的值(futex 值) 是否與 val 相等:
futex_wait_queu
中等待被喚醒。在 futex.h 中的 futex_wake 實作部分,透過 syscall 方式呼叫 Linux Kernel 中的 futex_wake
喚醒 limit 個等待者。
futex.h 中的 limit 對應 Linux kernel 中的 nr_wake,透過 plist_for_each_entry_safe
來決定要喚醒哪些等待者,在執行 plist_for_each_entry_safe
前,使用 spin_lock 方式,進行 lock 避免喚醒的等待者順序錯誤,將欲喚醒的等待者,放入 wake_q
中,最後透過 wake_up_q 來進行喚醒。
在 futex.h 中的 futex_wake 實作部分,會最多喚醒 limit 等待者,和 wait 不同的是,如果 futex 中的等待者超過 limit 個,
則超過的部分,會在對應的 wait queue 中等待,在 syscall 中的 INT_MAX 代表可以從 futex 中轉移到 wait queue 的最大數量。
根據 futex(2) 的敘述,futex 會一次喚醒 uaddr1 中的所有等待者,如果超過 nr_wake 個等待者時,會將剩餘的且數量不超過 nr_requeue 等待者放入 uaddr2 的 wait queue 中,這樣的做法是避免 thundering herd(驚群效應)。
固定設定的參數 nr_wake = 1, nr_requeue = INT_MAX,代表只會喚醒一個執行緒去競爭 uaddr2 對應的 futex,剩餘的放入 uaddr2 對應的 wait queue
mutex.h 中有三種 lock 操作,mutex_trylock
、mutex_lock
、mutex_unlock
先用 atomic_load
取得 mutex 狀態
fetch_or
對 mutex 進行上鎖。atomic_fetch_or_explicit
,此時 mutex->state 與 MUTEX_LOCKED 進行 or,並返回 mutex->state 的舊值。透過這樣的方式,在上鎖時經由下一行(8)同步確認是否已經被其他人給上鎖,如果已經被上鎖,則此次行為是失敗的回傳 false。
當上鎖成功後,透過 atomic_thread_fence
進行 mutex 狀態的同步。
mutex_lock
在最一開始的 for loop,不斷地透過 mutex_trylock
來持有鎖。
如果一直得不到鎖,則透過 atomic_exchange_explicit
將 state 狀態改成 MUTEX_LOCKED | MUTEX_SLEEPING
,並回傳先前的 state 值。
將 mutex->state 設定為 0,如果上鎖時有等待者,
則透過 futex_wake 來喚醒等待者。
cond.h 有三種操作,cond_wait
、cond_signal
、cond_broadcast
透過 load 方式獲取 cond->seq,透過 mutex 方式,避免 futex 系統呼叫的可能性,在有限次數上,不斷地監看 cond->seq 的值改變。
COND_SPINS
次後,仍然失敗,則回到 futex_wait
進行等待。隨後進入 while loop 每次會檢查狀態是否上鎖,如果上鎖,會透過 futex_wait 放入 wait queue 中等待。
參考 RinHizakura 同學的
在 fetch_or 地方,對於 mutex->state 添加MUTEX_SLEEPING
,是為了在之後mutex_unlock
時都能確保呼叫futex_wake
將 cond->seq 進行 + 1後,將對應的 cond->seq 給喚醒
對 cond->seq 進行 + 1 後,透過 futex_requee 喚醒等待者,進行查看
有兩個 struct node 與 clock
clock:內部紀錄 ticks,作為時間點的判斷,每一個 thread 接會根據 ticks 的值而作相應的行爲
node:為工作節點,建立後轉交給 thread 進行操作,一個 thread 負責一個 node
皆需要 conditional variable 和 mutex 機制,保護 clock->ticks 與 node->ready
clock 有三個操作 clock_wait
、clock_tick
、clock_stop
使用 mutex 進行上鎖,藉此檢查 clock->ticks 的值,如果 clock->ticks 小於 ticks 時,則進入 cond_wait
進行監看 ticks 是否改變
使用 mutex 進行上鎖,對於 ticks 進行 + 1,之後透過 cond_broadcast 進行廣播告知 clock->ticks 已經更新
使用 mutex 進行上鎖,對於 ticks 設定為 - 1,使得 clock->ticks 的累加停止,之後透過 cond_broadcast 進行廣播告知 clock->ticks 已經更新
clock 有二個操作 node_wait
、node_signal
,該 node 有幾個特性:
檢查 node 的 ready 狀態,若為 false,則進入 cond_wait 進行等待
將 ready 設定為 true 後,透過 cond_signal 喚醒等待者。
main function 如下,建立一個 clock,並新增 16 個 nodes ()
對於每一個 node 均建立一個 thread,並透過 thread_func
進行測試。
在建立 thread 後,透過 clock_tick
、clock_wait
、clock_stop
進行操作。
透過呼叫第一個 clock_tick
使得 ticks變為 1,藉此讓其他的 thread 可以開始根據 tick 逐步進行。
clock_wait
會一直等待 tick 變成 1 << N_NODES 後再透過 clock_stop
來讓 node 的 thread 結束
thread 在 clock->tick 小於 i 時,會進行等待,開始執行後,透過 node_wait
檢查該 node 的 parent 是否 ready,接著若 bit 是 false 則執行 clock_tick
進行 tick ++,若為 true,則透過 node_signal 喚醒下一個 thread。
修改第 1 次作業的測驗 γ 提供的並行版本快速排序法實作,使其得以搭配上述 futex 程式碼運作
修改方法並沒有很複雜
將 pthread_mutex_t
以及 pthread_cond_t
修改成 mutex_t
與 cond_t
在後續的程式部分,更換如下
pthread_mutex_init
-> mutex_init
pthread_mutex_lock
-> mutex_lock
pthread_mutex_unlock
-> mutex_unlock
pthread_cond_init
-> cond_init
pthread_cond_signal
-> cond_signal
pthread_cond_wait
-> cond_wait
下圖為 第 1 次作業的測驗 γ (mt 橘色)與修改成 futex 版本(ft 藍色),x 軸為 thread 數量,y 軸為執行時間。
每次新增 thread 會重複執行 100 次取平均時間,最後結果如圖,由圖可知換成 futex 後整體執行時間增加很多,且隨著 thread 增加執行時間有越長的趨勢。
後來發現是我編譯時,多加了-fsanitize=thread。
正確的
經過修正後如圖