--- tags: linux2023 --- # [2023 年暑期 Linux 核心課程](https://hackmd.io/@sysprog/linux2023-summer)第 1 次測驗/作業檢討 > [題目](https://hackmd.io/@sysprog/linux2023-summer-quiz0) ## 2011eric ```c static inline void cond_wait(cond_t *cond, mutex_t *mutex) { int seq = load(&cond->seq, relaxed); mutex_unlock(mutex); #define COND_SPINS 128 for (int i = 0; i < COND_SPINS; ++i) { if (load(&cond->seq, relaxed) != seq) { mutex_lock(mutex); return; } spin_hint(); /* 避免頻繁使用 atomics */ } futex_wait(&cond->seq, seq); mutex_lock(mutex); fetch_or(&mutex->state, MUTEX_SLEEPING, relaxed); } ``` ## RinHizakura > [linux-summer-2023: hw1](https://github.com/RinHizakura/linux-summer-2023/tree/main/hw1/treeint) 重新定義 S-tree 提供的 public interface。 ```c struct st_tree *st_create(cmp_t *cmp, struct st_node *(*create_node)(void *key), void (*destroy_node)(struct st_node *n)); void st_destroy(struct st_tree *tree); int st_insert(struct st_tree *tree, void *key); int st_remove(struct st_tree *tree, void *key); struct st_node *st_find(struct st_tree *tree, void *key); ``` 可在 `st_create` 中看到節點之間的 order 比較(`cmp`)、節點的建立與銷毀(`create_node`/`destroy_node`) 被從 S-tree 中獨立出來,以 callback function 的方式讓函式庫的使用者自行實作。其餘包含樹的走訪邏輯、root node 的維護等都直接在內部完成。 順帶一提,原本 `treeint_insert` 和 `treeint_remove` 中走訪 S-tree 的邏輯其實很相似。這裡我們實際上也可以重用 `st_find` 函式來歸納之,避免重寫相同的邏輯,以減少程式碼編譯後產生之二進位檔的大小。 完整的實作改進可以參照 [s_tree.c](https://github.com/RinHizakura/linux-summer-2023/blob/main/hw1/treeint/s_tree.c)。有了這項調整,未來就可以很輕易地將 S-tree 應用在其他合適的專案之中了! :::warning 雖然介面上變得簡單許多,不過其實如果對照 Linux 的 rbtree 實作,會發現其反而避免使用了使用 callback 的方式,作法比較接近本題原始的實作。 原因可參照原作者在 [rbtree.h](https://elixir.bootlin.com/linux/latest/source/include/linux/rbtree.h#L9) 中的註解說明: > To use rbtrees you'll have to implement your own insert and search cores. This will avoid us to use callbacks and to drop drammatically performances. I know it's not the cleaner way, but in C (not in C++) to get performances and genericity... ::: ### 探討 S-tree 與 rbtree 效能落差 首先,為利於後續加入其他演算法進行效能評比,對原始程式碼做出以下改動,以建立相關基礎建設: 1. 加入一個 macro `benchmark`,方便測量一段程式碼完成的時間 > 主要測量 insert/find/remove 三種操作的時間 2. 撰寫一個簡單的 python script `tree-perf.py`,方便將測量結果視覺化 > 為了讓測量結果盡量不受 noise 干擾,且視覺化後的曲線圖可以更平滑,試著用簡單的標準差法排除蒐集到的時間中之 outlier 3. 將 BST 的各項操作也抽象成 callback function pointer > 雖然前面提到 callback 的方式可能會對函式呼叫整體造成額外成本,但這裡我們主要的目標是要比較不同 tree 的 throughput,而非注重單一 tree 本身在實際應用上的可能優化 > 因此取捨傾向先以能夠容易且快速的加入各種 BST 實作的設計方式為主 效能評比程式有了,那麼 rbtree 的函式庫該從何而來呢? 稍微搜尋了一下,一方面似乎沒有比較被大眾廣泛接受的紅黑樹相關 C 函式庫;一方面,S-tree 的實作和 Linux 中的 rbtree 介面上其實存在不少相似之處。因此我們乾脆將 Linux 的紅黑樹實作拉到 userspace 來應用。這裡參照 Linux 6.4 版本中的程式碼,並且只擷取與 insert/find/remove 三項操作相關的函式實作,具體整理出來的程式碼可以參考: * [rbtree.h](https://github.com/RinHizakura/linux-summer-2023/blob/main/hw1/treeint/rbtree.h) * [rbtree.c](https://github.com/RinHizakura/linux-summer-2023/blob/main/hw1/treeint/rbtree.c) 接著我們就可以來測量各個操作間的差異。 將實驗分為循序輸入和亂序輸入的場景。對於循序輸入的部份,我們將從 `0` 一直到 `tree_size - 1` 的 key 值插入到樹中,然後再以相同的次序進行搜尋和刪除。過程記錄不同的樹節點數量與完成相應操作所需之累積時間的關係。則實驗的結果如下圖: ![](https://hackmd.io/_uploads/SkDF9RJ22.png =400x) 循序狀況下,似乎在樹節點數量較少時兩者差異不大,而節點增加時,S-tree 相較於 rbtree 的表現更好? 而亂序輸入的場景下,我們會隨機產生任意的插入/搜尋/移除值,實驗結果如下: ![](https://hackmd.io/_uploads/BkhEjAkhn.png =400x) 從結果來看,亂序狀況下,S-tree 整體的效能看起來稍優於 rbtree。且在亂序情形下 rbtree 似乎更容易有 worst case (圖中極端值) 的出現。 該怎麼依此給出合理的解釋呢? * 需注意到其實無論是插入/搜尋/刪除操作,其實三者都具備**搜尋**的部份,而插入和刪除只是多了調整節點間關係+旋轉的步驟 * 則如果瓶頸發生在搜尋部份,可以預期三種操作測量出的時間曲線圖將很相似(例如本實驗)? * 對於亂序下出現特殊的極端值(圖中的尖峰),可能是對某個 key 值的搜尋會導致最差情況(worst case)的發生? * 不認為是硬體(CPU、Cache)干擾的原因在於三種操作在同一節點數量都出現極端值 * 理論上,在樹的平衡上 S-tree 應具有較為明顯的優勢,如下圖以循序插入 0 - 9 的節點為例,可見在限制樹高度的能力上 S-tree 是更有效的 :::warning 注意退化的狀況,worst case 的討論 ::: ---- ## fourcolor * AVL tree 可以確保每個節點的左右子樹高相差不超過 1 ,因此每個節點需要儲存當前高度來計算 balance factor 來決定是否需要 rotate。 * red-black tree 則藉由 1) 不能有兩個紅色node相連 2) 所有從該 node 走到其 subtree 下的 leaf 的上之黑色 node 數必定相同的規則來達到平衡。 * S-Tree 的平衡機制與 AVL tree 較為相像利用 hint 來評估是否需要做 rotate ,然而 st_node 的 hint 的計算方式為其左右節點 hint 最大值 +1 並且只有在被呼叫到 st_update 時才會被更新。經過實驗觀察,發現 S-Tree 的 worst case 會是 O(n) 並且 worst case 的情形會發生在第一個 a 值極大,接著一串不大於 a 的遞增數列(反之亦然) ```c int a[] = {1000, 1, 2, 8, 16, 33, 73, 82, 83, 91, 95}; for (int i = 0; i < ARRAY_SIZE(a); i++) { treeint_insert(a[i]); } treeprint(st_root(tree), 0); /* 簡單的圖示,無左右節點之分 1 ∟———2 ∟———8 ∟———16 ∟———33 ∟———73 ∟———82 ∟———83 ∟———91 ∟———1000 ͱ———95 */ ``` 我們假設目前的 S-Tree 已經有 1000, 1, 2, 8 插入,接著插入 16 來觀察 1. `st_insert` 前 ```graphviz digraph Tree{ graph[ordering=out] null0 [shape=point][color=white] null1 [shape=point][color=white] null2 [shape=point][color=white] 1 -> 2 1 -> null0[style=invis] 2 -> 1000 2 -> null1[style=invis] 1000 -> null2[style=invis] 1000 -> 8 } ``` 2. `st_update` 前 ```graphviz digraph Tree{ graph[ordering=out] null0 [shape=point][color=white] null1 [shape=point][color=white] null2 [shape=point][color=white] null3 [shape=point][color=white] 1 -> 2 1 -> null0[style=invis] 2 -> 1000 2 -> null1[style=invis] 1000 -> null2[style=invis] 1000 -> 8 8 -> 16 8 -> null3[style=invis] } ``` 3. 對 16 做 `st_update`: 由於 16 沒有左右節點因此不會做任何動作,但是由於 16 的 hint 為 0 所以會對其親節 8 點做 update。 4. 對 8 做 `st_update`: 由於 st_balance 為 1 因此不會做任何動作,但由於 8 的 hint 有經過更改 (0 -> 1) 所以會對其親節 1000 點做 update。 4. 對 1000 做 `st_update`: 做完 rotate 後,由於 1000 的 hint 沒有改變 (1->1) 因此結束。 ```graphviz digraph Tree{ graph[ordering=out] null0 [shape=point][color=white] null1 [shape=point][color=white] null2 [shape=point][color=white] null3 [shape=point][color=white] 1 -> 2 1 -> null0[style=invis] 2 -> 8 2 -> null1[style=invis] 8 -> 1000 8 -> null3[style=invis] 1000 -> null2[style=invis] 1000 -> 16 } ``` 從上面會發現由於 hint 本身只要沒有改變且不等於 0 就不會往上 update ,代表 S-Tree 只要 update leaf 時發生 AVL 的 LR 或是 RL 就不會再往上 update 了 --- ## DrXiao S-Tree 效仿 Linux 核心的 linked list,而定義了以下的結構體以及操作函式、巨集(此處僅列舉部份函式、巨集) ```c struct st_node { short hint; struct st_node *parent; struct st_node *left, *right; }; struct st_root { struct st_node *root; }; void st_insert(struct st_node **root, struct st_node *p, struct st_node *n, enum st_dir d); void st_remote(struct st_node **root, struct st_node *del); ``` 可見 ```st_node``` 中,先撇除 ```hint``` 物件(會在後續說明用意),剩下的物件也都是指標,分別是指向父節點、左側子節點以及右側的子節點。參考 Linux 核心的 linked list 設計理念,不難推論 S-Tree 想達成以下示意圖的效果: ![](https://hackmd.io/_uploads/SJB_xx6sh.png) 同樣參照 [Linux 核心原始程式碼巨集: ```container_of```](https://hackmd.io/@sysprog/linux-macro-containerof) ,可發現與 linked list 的效果有異曲同工之妙。S-Tree 同樣提供一內含數個指標的結構以及相關的操作函式,讓開發者引用 ```st_node```、```st_root``` 來自行定義二元搜尋樹所需的 ```struct```,而涉及二元樹的**插入新節點**、**刪除指定節點**的操作,可利用 ```st_insert()```、```st_remove()``` 兩個函式,來完成增減節點的操作函式,而程式碼便不用再撰寫指標操作相關的部份,因為一切交由 ```st_insert()``` 和 ```st_remove()``` 來處理,讓每一節點中的 ```parent```、```left```、```right``` 物件,都可以指向應指向的節點。如此,開發者即可建立適當的 S-Tree。 --- ## StevenChen8759 ```c align_up(120, 4) = 120 align_up(121, 4) = 124 align_up(122, 4) = 124 align_up(123, 4) = 124 ``` * 推導為何 align 的計算 * 首先,我們令 $sz = A*n + r$,其中$A \in \mathbb{Z}^+,\text{ } n \in \mathbb{N},\text{ } r \in \{0, 1, 2, ..., A-1\}$ * 上列公式中,$A$ 為 alignment count,$n$ 與 $r$ 為變動值,使 sz 可為任意 Size (包含 0)。 * 令 Function $align\_up(sz,A)$ 為求 alignment 的公式,並以上述的範例輸出觀察 $align_up$ 公式須具備的特性: * 120 恰可被 4 整除,此時 $A = 4, n = 30, r = 0$,結果 $120 = 4 * 30$ * 121 除以 4 餘數 1,此時 $A = 4, n = 30, r = 1$,結果 $124 = 4 * 31$ * 122 除以 4 餘數 2,此時 $A = 4, n = 30, r = 2$,結果 $124 = 4 * 31$ * 123 除以 4 餘數 3,此時 $A = 4, n = 30, r = 3$,結果 $124 = 4 * 31$ * 124 除以 4 餘數 0,此時 $A = 4, n = 31, r = 0$,結果 $124 = 4 * 31$ * 125 除以 4 餘數 1,此時 $A = 4, n = 31, r = 1$,結果 $128 = 4 * 32$ * 綜上整理可觀察到當 $r \in \{1,2,3\}$ 時,我們最後的輸出會變成 $31 = 30 + 1$,考量上列特性,可得下列公式: * $align\_up(sz,A)=\left\{ \begin{array}{l} A*n, \text{if } r = 0 \\ A*(n + 1), \text{if } r \in \{1, 2, ..., A-1\} \end{array} \right.$ * 因上述公式適用除法原理,我們在此運用 ceiling function 取得 align up 的 size,可將上述公式改寫如下: * $align\_up(sz,A)=\lceil sz/A \rceil*A =\lceil n + \frac{r}{A} \rceil*A = (n + \lceil \frac{r}{A} \rceil) * A$ * 考量除法運算的特性,以及後續 bitwise operation 的最佳化,我們改寫上述公式為取 floor function 的實作,此時須在 $\lceil \frac{r}{A} \rceil$ 項的分子加上一常數 $c$,改寫後如下所示: * $align\_up(sz,A) = (\lfloor \frac{A*n+r+c}{A} \rfloor) * A = (n + \lfloor \frac{r+c}{A} \rfloor) * A$ * 接下來我們求常數 $c$ 之值,還原至最初的 $align\_up$ function 搭配兩式不同的條件,可得到下列聯立方程式 (推導過程省略): * $\left\{ \begin{array}{l} \lfloor \frac{c}{A} \rfloor = 0, \text{if } r = 0 \\ \lfloor \frac{r+c}{A} \rfloor = 1, \text{if } r \in \{1, 2, ..., A-1\} \end{array} \right.$ * 根據 floor function 的特性,我們可得到下列聯立不等式: * $\left\{ \begin{array}{l} c \leq A-1, \text{if } r = 0 \\ A \leq (r+c) \leq (2*A - 1), \text{if } r \in \{1, 2, ..., A-1\} \end{array} \right.$ * 上列第二式的條件 $r \in \{1, 2, ..., A-1\}$ 可再寫成不等式 $1 \leq r \leq A-1$,並拓展為 $(1 + c) \leq (r + c) \leq (A-1+c)$,我們拆解這些條件,並考量上述 floor function 的特性重組成下列兩組聯立不等式: * $\left\{ \begin{array}{l} (r + c) \leq (A - 1 + c) \\ (r + c) \leq 2*A - 1 \end{array} \right.$,相減得到 $c \leq A$ * $\left\{ \begin{array}{l} (r + c) \geq (1 + c) \\ (r + c) \geq A \end{array} \right.$,相減得到 $c \geq A - 1$ * 總結所有常數 $c$ 的不等式,聯立如下:最終 $c = A - 1$ * $\left\{ \begin{array}{l} c \leq A-1\\ c \leq A \\ c \geq A - 1 \end{array} \right.$,求得 $c =A - 1$ * 我們現在求得最終的 $align\_up(sz,A)=(\lfloor \frac{sz+(A-1)}{A} \rfloor) * A$,對應至程式碼之實作即為 `(((sz + mask) / alignment) * alignment)`,其中 `mask = alignment - 1`。 * 當 alignment $A$ 為 2 的冪時,我們令 $A = 2^k$,其中 $k$ 為正整數,此時公式變成 $align\_up(sz,2^k)=(\lfloor \frac{sz+(2^k-1)}{2^k} \rfloor) * 2^k$,因此我們可利用右移與左移實現乘以與除以 $2^k$,可得 `(((sz + mask) >> alignment) << alignment)`,其中 `mask = alignment - 1`,考量 pattern 中不可出現 alignment,可改寫成 `(((sz + mask) >> (mask + 1)) << (mask + 1))`。 * 但顯然上述的 pattern 看起來有些臃腫,我們再深入觀察二進位數值系統的特性,當我們對 size 除以 2^k 時,假設 $(sz + (2^k-1)) = 2^k*n+r$,則其餘數 $r \in \{0, 1, 2,...,2^k-1\}$,因此餘數部分恰好可用 k bit 表示之,這部分恰為 $(sz + (2^k-1))$ 的 last k bit,因此直接取 k + 1 以上的 bit 並右移 k bit 即為 $(\lfloor \frac{(sz+(2^k-1))}{2^k} \rfloor)$ 的結果,但因最後還要再乘上 $2^k$,相當於再左移 k bit,恰好與前述的右移 k bit 互消,但考量 last k bit 必定為 0,因此需使用 bit mask 將 last k bit 設置為 0。 * 以 4 Byte alignment 為例,我們須設定 last 2 bit 為 0,因 mask 為 0x03,以 ~mask 做 and 操作即可完成設定,並保留 bit 3 以上的原始值。 * 最後的實作為 `((sz + mask) & ~mask)`,其中 `mask = alignment - 1` --- ## NOVBobLee * 使用 [`TSan`(`ThreadSanitizer`)](https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual) 編譯,在之後程式執行時可檢查 data race 的發生。 ``` $ gcc -std=gnu11 -Wall -g -fsanitize=thread -o qsort_mt qsort_mt.c -lpthread ``` * 執行程式 (使用兩個執行緒), `TSan` 警告有 data race 發生。 ``` ./qsort_mt -tv -f2 -h2 -n100 ================== WARNING: ThreadSanitizer: data race (pid=5885) Write of size 4 at 0x7b4000000080 by thread T1 (mutexes: write M0): #0 allocate_thread /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:211 (qsort_mt+0x31f3) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #1 qsort_algo /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:314 (qsort_mt+0x3f0e) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #2 qsort_thread /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:352 (qsort_mt+0x4328) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) Previous read of size 4 at 0x7b4000000080 by thread T2 (mutexes: write M1): #0 qsort_thread /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:343 (qsort_mt+0x423e) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) Location is heap block of size 256 at 0x7b4000000000 allocated by main thread: #0 calloc /usr/src/debug/gcc/gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:701 (libtsan.so.2+0x43413) (BuildId: 7e8fcb9ed0a63b98f2293e37c92ac955413efd9e) #1 qsort_mt /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:132 (qsort_mt+0x2677) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #2 main /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:505 (qsort_mt+0x4d7d) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) Mutex M0 (0x7ffeb8a182d8) created at: #0 pthread_mutex_init /usr/src/debug/gcc/gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1329 (libtsan.so.2+0x448c6) (BuildId: 7e8fcb9ed0a63b98f2293e37c92ac955413efd9e) #1 qsort_mt /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:130 (qsort_mt+0x265a) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #2 main /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:505 (qsort_mt+0x4d7d) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) Mutex M1 (0x7b40000000a8) created at: #0 pthread_mutex_init /usr/src/debug/gcc/gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1329 (libtsan.so.2+0x448c6) (BuildId: 7e8fcb9ed0a63b98f2293e37c92ac955413efd9e) #1 qsort_mt /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:136 (qsort_mt+0x26fd) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #2 main /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:505 (qsort_mt+0x4d7d) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) Thread T1 (tid=5887, running) created by main thread at: #0 pthread_create /usr/src/debug/gcc/gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1036 (libtsan.so.2+0x44219) (BuildId: 7e8fcb9ed0a63b98f2293e37c92ac955413efd9e) #1 qsort_mt /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:144 (qsort_mt+0x280c) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #2 main /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:505 (qsort_mt+0x4d7d) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) Thread T2 (tid=5888, running) created by main thread at: #0 pthread_create /usr/src/debug/gcc/gcc/libsanitizer/tsan/tsan_interceptors_posix.cpp:1036 (libtsan.so.2+0x44219) (BuildId: 7e8fcb9ed0a63b98f2293e37c92ac955413efd9e) #1 qsort_mt /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:144 (qsort_mt+0x280c) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) #2 main /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:505 (qsort_mt+0x4d7d) (BuildId: 73768f1e290a7297a057e88a4b073661ed3575c4) SUMMARY: ThreadSanitizer: data race /home/zz4t/test/ttt/sysprog2023/quiz0/qsort_mt.c:211 in allocate_thread ================== 0.0254 0.017 0.0102 ThreadSanitizer: reported 1 warnings ``` 從訊息中可以得到 data race 發生的記憶體位置為 `0x7b4000000000` ,在 `qsort_mt.c:132` 配置的 `c.pool` 之中。 ```c=132 if ((c.pool = calloc(maxthreads, sizeof(struct qsort))) == NULL) ``` `c.pool` 為 `struct qsort` 組成之陣列,其中的成員 `st` 應該要被 `mtx_st` 所保護。 ```c=88 struct qsort { enum thread_state st; /* For coordinating work. */ struct common *common; /* Common shared elements. */ void *a; /* Array base. */ size_t n; /* Number of elements. */ pthread_t id; /* Thread id. */ pthread_mutex_t mtx_st; /* For signalling state change. */ pthread_cond_t cond_st; /* For signalling state change. */ }; ``` 但在 `qsort_mt.c:211` 的位置進行 `st` 的寫入動作前,並未持有 `mtx_st` ,與 `qsort_mt.c:343` 位置的讀取動作有 data race 發生的可能。 ```c=205 static struct qsort *allocate_thread(struct common *c) { verify(pthread_mutex_lock(&c->mtx_al)); for (int i = 0; i < c->nthreads; i++) if (c->pool[i].st == ts_idle) { c->idlethreads--; c->pool[i].st = ts_work; /* st write */ verify(pthread_mutex_lock(&c->pool[i].mtx_st)); /* mtx_st acquire */ verify(pthread_mutex_unlock(&c->mtx_al)); return (&c->pool[i]); } verify(pthread_mutex_unlock(&c->mtx_al)); return (NULL); } ``` ```c=341 /* Wait for work to be allocated. */ verify(pthread_mutex_lock(&qs->mtx_st)); while (qs->st == ts_idle) /* st read */ verify(pthread_cond_wait(&qs->cond_st, &qs->mtx_st)); ``` 修正很簡單,就是在 `st` 執行寫入動作前,先爭取到 `mtx_st` 即可。 ```diff --- a/quiz0/qsort_mt.c +++ b/quiz0/qsort_mt.c @@ -208,8 +208,8 @@ static struct qsort *allocate_thread(struct common *c) for (int i = 0; i < c->nthreads; i++) if (c->pool[i].st == ts_idle) { c->idlethreads--; - c->pool[i].st = ts_work; verify(pthread_mutex_lock(&c->pool[i].mtx_st)); + c->pool[i].st = ts_work; verify(pthread_mutex_unlock(&c->mtx_al)); return (&c->pool[i]); } ```