2015年2月28日 星期六

[books] - pthread 多緒程式設計



平時有準備, 就不怕書絕版, 對於 thread, 我不是很熟悉, 它很複雜, 也是我少有接觸的領域, 不過這次躲不過了, 我決定好好的來了解一下 thread。

不知道這本書是來的太早還是太晚, 中文版本在 199711 出版, 英文版 1996 出版, windows 95 正好流行於這年代, windows 95 帶來不少技術, thread 是其中之一, 而 thread 開始變成人人朗朗上口的名詞, 好像不知道這名詞, 又要被人嘲笑, 今年已經是 2015 年, 在程式設計圈, thread 已經是無人不知的名詞了。根據 The history of threads 似乎在 1965 就有 thread 這樣的概念了。

Pthreads Programming: A POSIX Standard for Better Multiprocessing (O'Reilly Nutshell) Bradford Nichols, et al Sold by: Amazon.com Services, Inc. $24.46 20020130 購於 us amazon

這是爭議很大的出版社 - 松格出版的, 該出版社已經消失了。譯者是蕭伯剛, 台大資工升四年級暑假翻譯的, 厲害; 不是因為他讀台大而厲害, 而是把這本書翻的很好, 除了因為是台大的高材生之外, 當然也是因為他掌握了 thread, 文字很通順, 很口語化, 例如: p238 「一昧的相信函式庫只會導致你除錯除到吐血為止」, 而保留英文術語不譯, 這是我喜歡的翻譯方式。我在 201410 左右才開始讀這本書, 看來準備的英文版本應該派不上用場。

最近閱讀的都是簡體中文的書籍, 繁體中文的電腦技術書籍在質與量上已經和中國愈差愈遠了, 這本還是 1997 年出版的, 台灣出版社加加油。

pthread 發展了這麼長的一段時間, 有很多機制都已經幫程式員想好了, 我們只要知道這些東西如何使用, 幾乎就可以處理大部份惱人的 thread 問題, 這本書就是在介紹這些東西。

另外一本好書是《Win32 多緒程式設計》, 我一直以為我會由這本 thread 書籍來學習 thread programming, 畢竟侯老師比較有知名度, 書的質量也很好, 但我顯少使用 windows, 就當作藏書囉!


有時候會收到是否要賣書的 mail, 好書難得, 這些都是我的珍藏, 捨不得賣。我自己也有好多好書因為絕版買不到而鎚心肝, 我能理解你的感受。

晤 ... 這本竟然能賣到 2000 元, 好書難求, 絕版好書更是難尋, 不過我不想當冤大頭, 我有另外的方法求書。



定價 500, 我應該是買 8 折吧, 英文版在台灣拍賣上便宜點 7xx nt, 不過對使用中文的你我來說, 中文版才更具價值, 尤其是好的翻譯本, 好書再貴都有人要買。

每個專業人士都會有自己的工具箱, 頂層是什麼、中間層放什麼、最下層又是哪些玩意兒, 各憑自己的規劃, 你的工具箱裡頭只有 google 嗎? 也許你太高估自己了。我知道有初學 c 語言的人 (你看他們在論壇問的問題就知道了, 完全就是沒看書所提的問題, 也難怪會有人不高興了), 憑著 google 找資料學習, 不知道為什麼他們要省下這書錢, 就算是初學等級的書籍, 也會有足以收藏一輩子的大作。

你說書會過時, 尤其是在這快速更新的 it 產業, 我承認, 那你覺得這本 1997 的 pthread programming 有過時嗎? 也許你可以看完本文後再思考一下答案, 選擇一本好書並沒有想像中的容易, 買到爛書就容易不少, 我對選書頗有自信, 本本收藏的都是好書。

本書程式碼的排版很糟糕, 有不少程式書籍都這樣, 程式碼連行號都沒有, 我真是搞不懂排個程式碼有什麼難的 (別說書了, 我連在 blog 的程式碼都有行號), 會比做 index 還難嗎? 不過這本特別糟, 有的 if/else 沒對好, 也沒有行號, 好像是手工打的, 有些還有錯誤, 需要對照範例程式碼免得被誤導。OH! 松格阿松格 ... 你是怎麼做書的阿?

我已經瞭解 process, thread 也在我想像中, 對 os 來說, process 可以想成是 thread。但只有這樣的認識並不足以寫出執行良好的 thread 程式, 甚至是只能正常的執行 thread, 更別說要以 thread 增進效能; 但僅僅只是要寫出正確的 thread 程式, 基本要求也不少, 需要知道一些 thread 的基本觀念。

chapter 1 介紹了什麼是 thread, 什麼樣的程式才能用 thread 加速, 當然你想用 thread 來減速程式的執行也不是不行。還介紹了使用 multi-process 和 mulit-thread 的範例, 讓讀者們對這兩種程式有所了解。

而 cpu 若只有單核心, 有些程式就算用了 thread 也是無法加速的, 若對這樣的觀念沒有清楚認識, 可能會誤用 thread, 基本上需要等某些事情發生的程式, thread 都可以幫上忙; 至於在多 cpu 上, thread 自然就如魚得水了。

chapter 2 說明 thread 的使用模型, 這是經驗的累積, 也許在不同的工作上, 你自己會發現自己的使用模型。

  1. boss/worker mode
  2. peer mode
  3. pipeline mode
一旦你的需求和這些模型類似, 就可能可以採取這樣的方式來設計你的 thread 程式。

本章有兩個範例: 一個銀行的 atm 程式, 先來一個循序式的版本, 再提供 multi-thread 版本。這個 atm 程式使用的是 boss/worker mode, 也就是 boss 這個 thread 到處接工作, 產生 worker thread 讓他們有事情做, 這樣公司才有收入可以發薪水給他們。

另外一個是矩陣運算, 也是一個循序式的版本, 一個 multi-thread 版本。這樣可以讓我們比較 multi-thread 帶來的好處。

chapter3 介紹 thread 的同步機制:
  1. pthread_join
  2. mutex
  3. condition variable (條件變數)
  4. pthread_once
你知道幾樣呢? 如果你只知道 mutex, 那你知道為什麼有這本書了, 350 頁可不是厚假的。這是複雜的一章, 談的就是 lock 機制。從最簡單的一個 mutex 開始, 到 mutex 和 condition variable 的合體, 再到好幾個 mutex 和好幾個 condition variable 一起使用。一旦有哪個 lock 和 unlock 沒配合好, 這個 thread 程式就要出錯, 腦袋得很清楚才行。lock 多了, 嗯 ... 程式很安全, 但效能不好, 還不如不要用 thread, 累死自己又懷疑怎麼 thread 都沒能增加效能, 增加的只有笑能吧! 書的範例提供一些技巧, 但手上的程式碼, 總是無法像書中那般理想, 得自己想辦法, 證明自己的價值。

p106 解釋了為什麼 condition variable 要和 mutex 一起使用, 和 condition variable 相關的 function 有 pthread_cond_signal(), pthread_cond_wait()。

3-7 的程式碼在中文版和英文版都有錯誤, 我修改了一份放在這裡:

ex 3-7: cvsimple.c
 1 // Pthreads 多緒程式設計 page 102: 3-7 
 2 // ref: http://maxim.int.ru/bookshelf/PthreadsProgram/htm/r_28.html
 3 // fixed syntax error, let it can be compiled.
 4 
 5 #include <stdio.h>	
 6 #include <pthread.h>	
 7 #define TCOUNT 10	
 8 #define WATCH_COUNT 12	
 9 
10 int count = 0;	
11 pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;	
12 pthread_cond_t count_threshold_cv = PTHREAD_COND_INITIALIZER;	
13 int  thread_ids[3] = {0,1,2};	
14 
15 typedef void *(*ThreadFunc) (void *);
16 
17 void watch_count(int *idp)	
18 {	
19   pthread_mutex_lock(&count_mutex);
20 
21   while (count <= WATCH_COUNT) 
22   {	
23     pthread_cond_wait(&count_threshold_cv, &count_mutex);	
24     printf("watch_count(): Thread %d, Count is %d\n", *idp, count);	
25   }	
26   pthread_mutex_unlock(&count_mutex);	
27 }	
28 
29 void inc_count(int *idp)	
30 {	
31   int i=0;
32 
33   for (i =0; i < TCOUNT; i++) 
34   {	
35     pthread_mutex_lock(&count_mutex);	
36     count++;	
37     printf("inc_count(): Thread %d, old count %d, new count %d\n", *idp, count - 1, count );	
38     if (count == WATCH_COUNT)	
39       pthread_cond_signal(&count_threshold_cv);	
40     pthread_mutex_unlock(&count_mutex);	
41   }	
42 }	
43 
44 int main(int argc, char *argv[])
45 {	
46   int i;	
47   pthread_t threads[3];	
48   pthread_create(&threads[0], NULL, (ThreadFunc)inc_count, &thread_ids[0]);	
49   pthread_create(&threads[1], NULL, (ThreadFunc)inc_count, &thread_ids[1]);	
50   pthread_create(&threads[2], NULL, (ThreadFunc)watch_count, &thread_ids[2]);	
51   for (i = 0; i < 3; i++) 
52   {	
53     pthread_join(threads[i], NULL);	
54   }	
55   return 0;	
56 }	


my.c
1 void a_thread()
2 {
3   int x; 
4  pthread_mutex_lock(&mutex);
5    x=1;
6  pthread_cond_signal(&cond_var);
7  pthread_mutex_unlock(&mutex);
8 }

10 void b_thread()
11 {
12  int a,b,c;
13  pthread_mutex_lock(&mutex);
14  a=1;
15  b=2;
16  c=3;
17  pthread_cond_wait(&cond_var, &mutex);
21  //
22     unlock(&mutex)
23     wait();
24     ...
25     lock(&mutex);
26  //
27
18  pthread_mutex_unlock(&mutex);
19 }

為什麼 condition variable 要和 mutex 一起使用, my.c L21 ~ L27 是 pthread_cond_wait 展開的部份內容, pthread_cond_wait() 會先 lock mutex, 被喚醒之後再 unlock mutex, 假如執行到 L5, 表示 L17 pthread_cond_wait() 已經執行到 L23, 已經開始在等了, 否則, 就會等在 L4, 無法執行到 L5, 所以當執行到 L6 時, L24 就會開始執行, 因為被喚醒了, 這時候就會卡在 L25, 等到 L7 mutex 解開之後, L25 才能繼續下去, 這樣的搭配很順利。 而 my1.c 沒有 lock mutex 直接呼叫 L6 pthread_cond_signal(), 很有可能喚不醒 b_thread, 假設目前執行到 L15, 這時候執行到 L6, 而 L6 以後都不會在執行的話, 這時候 b_thread 就在也喚不醒了。

my1.c
1 void a_thread()
2 {
3   int x; 
5   x=1;
6   pthread_cond_signal(&cond_var);
8 }

10 void b_thread()
11 {
12  int a,b,c;
13  pthread_mutex_lock(&mutex);
14  a=1;
15  b=2;
16  c=3;
17  pthread_cond_wait(&cond_var, &mutex);
21  //
22     unlock(&mutex)
23     wait();
24     ...
25     lock(&mutex);
26  //
27
18  pthread_mutex_unlock(&mutex);
19 }

本章的例子是一個 linked list, 示範使用 mutex 來鎖住讀寫操作。condition variable 也用了一個簡單的範例來說明, 需搭配 mutex 來使用, 整個等待/喚起過程有點複雜, 牽扯到 mutex 的擁有與釋放。

本章最後提到如何限制 thread 的數量, ATM 伺服器關機時要如何處理 (這可不簡單哦!), thread pool 的建立方式。這些都不是很容易想到的技巧, 透過書本來學習這些前人的智慧, 輕鬆多了。

這章就幾乎把寫 thread 要注意的事項都提點完畢, 算是重點章節。

chapter 4 在闡述 thread 的管理, 一開始是 thread 屬性的設定, 例如設定某個 thread stack 的位置/大小。再來是 pthread_once 的用法。

Keys: Using Thread-Specific Data 的用法算是蠻好用的, 不過 thread function 都需要用到型別轉換, 應該會難倒 c 語言初學者。我當初對於 socket 的型別轉換也是傷透腦筋, 想不出來為什麼要這樣用。這類似 c++ std::map 可以用 key 來儲存/取得某個 thread 自己的資料結構。

這是另外一個 socket api 的例子

int getaddrinfo(const char *node, const char *service,
                       const struct addrinfo *hints,
                       struct addrinfo **res);


呼叫 getaddrinfo 之後, 還得記得要 freeaddrinfo(result), 而 res 本身又是個複雜的 linked list 資料結構, 有興趣可以參考 man page 的用法, 雖然難不倒你, 但用起來實在太複雜。

cancel thread 就是把某個 thread 結束掉, 不過 thread 並不是那麼單純可以直接結束掉, 要是這個 thread 擁有一個 mutex, 而沒有釋放就把它結束掉, 那問題就大了, 你不希望這樣的程式碼發生在核四上吧。

pthread 提供 cancelability type, state 來支援取消 thread, function 不難用, 難的是怎麼使用這些機制來讓 thread 程式運作正常, 這不是容易的事。

這裡的觀念是 cancelability type, state 和取消點, 當 cancelability type 是 PTHREAD_CANCEL_DEFERRED, 發動取消, 在程式碼的某些會發生取消的效果, 如果你沒預期到這些, thread 可能在你不知道的地方被結束了, 有可能 bug 就發生在這裡, 這是很難複製的 bug。

這節有個範例, 支援 atm server 的取消存款的動作, 仔細閱讀一番, 你會發現要考慮週到, 不是容易事。而這些程式碼都要付出代價, 若是不符合效益, 那還不如不要支援取消功能, 賠了夫人又折兵。

請參考這個範例: http://maxim.int.ru/bookshelf/PthreadsProgram/htm/r_36.html#854257

接著是 thread 的排程, 讀過有概念就好, 畢竟提供優先權這些屬性的成效, 只有試過才知道。比較重要的概念是 priority inversion, 若是一個優先權較低的 thread 握有一個 mutex, 那優先權高的 thread 也無技可施, 得等, pthread 提供了 pritority ceiling, priority inheritance 來處理這問題。這是把 mutex 和 thread 本身的優先權做個結合。

pritority ceiling: 如果擁有該 mutex 的 thread 優先權低於該 mutex, 會將該 thread 優先權提高和 mutex 一樣。

priority inheritance: 將持有這個 mutex 的 thread 優先權提高到和等待中的 thread 一樣。

page 209 有個發電廠的例子, 你一定馬上想到我們的核四會不會有同樣的問題吧!

chapter 5 談到和 unix 之間的互動, unix 有 fork, signal, 當這些東西通通和 thread 扯在一起的時候會怎麼樣呢? 會很麻煩, 麻煩到程式出錯時很難找到原因, 你得用猜的, 看是不是出在這些問題上。

我補充了一篇自己的心得: thread 和 signal

另外一個是在 thread 中使用 fork 的問題, 比較明顯的是 child process 繼承了 parent process 的 mutex, 而這個 mutex 是在 lock 的狀態, 對 child process 來說, 我還沒 lock mutex, 怎麼就已經 lock 住一個 mutex 了, 所以有個 pthread_atfork 來處理這問題。

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));


這個函式看來不好惹, 事實上也的確很複雜, 書中的提到的例子得要花點腦筋來理解。

chapter 6 講的東西比較雜, thread 怎麼實作: 有 kernel thread, user space thread, 還有把這兩種混合的 thread 實作。支援 thread 的 debugger (digit unix 上的除錯器)。比較各種情形下 multithread, multiprocess, single process 的執行效率, mulitthread 可不是完勝, 在某些情形下, 不使用 thread 反而是有比較好的效率。

後面三個附錄很輕鬆, 隨意瀏覽即可。

pthread 一樣有支援 spinlock, 本書少了 spinlock 這主題,《Pthreads 並行編程之 spin lock 與 mutex 性能對比分析》補上了這個漏網之魚。

不過很可惜, 雖然我得到這些知識, 但是在平常開發環境用不上這些東西, 而公司另外的團隊有這些需求, 他們會比較需要這些知識, 所以我也只有紙上知識, 沒能在工程上用到這些東西, 好羨慕他們可以碰到這些。

ref:

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。