※ [本文轉錄自 UNIX 看板] 發信人: Dracular.bbs@cis.nctu.edu.tw (吸 血 鬼), 看板: UNIX 標 題: Getting Started With POSIX Threads 中文版 發信站: 交大資科_BBS (Sat Jan 24 19:12:13 1998) 轉信站: fromzero!freebsd.ntu!nctumenews!ctu-peer!news.nctu!news.iim.nctu!news. Apparently-To: Getting Started With POSIX Threads Tom Wagner Don Towsley Department of Computer Science University of Massachusetts at Amherst 中文翻譯 宋振華 chsong@iim.nctu.edu.tw 國立交通大學 資訊管理研究所 http://www.iim.nctu.edu.tw/DOC/ProgTools/pthread.txt 1/23/1998 1.導論:thread 是什麼?為什麼要用 thread? Thread 通常被稱做輕量級的行程(Lightweight process;LWP),這個說法似乎 過於簡單了一些,但卻不失為一個好的起點; thread 是 UNIX process 的近親,但卻 不完全相同。為了說明何謂 thread ,我們必須先瞭解 UNIX process 與 Mach task 、 thread 間的關係。在 UNIX 中,一個 process 包括了一個執行中的程式,和一些 他所需的系統資源,諸如檔案描述表和位址空間等。但是在 Mach 中,一個 task 卻只 包括了一些系統資源; 而由thread 掌握了所有的執行活動。一個 Mach task 可能有 任意多個 thread , 而 Mach 系統中所有的 thread 均屬於一些 task。屬於同一個 task 的所有 thread 共享該 task 所擁有的系統資源。因此, thread 實質上就是一 個程式計數器、一個堆疊再加上一組暫存器。 UNIX 的一個 process 可以看成是一個 只有一個 thread 的 Mach task。 跟UNIX process 比起來, thread 是非常嬌小玲瓏的,因此對 CPU 而言,產生一 個 thread 是一件相對便宜的工作。另一方面,共享系統資源的 thread 跟獨佔系統資 源的 process 比起來,thread 也是相當節省記憶體的。 Mach thread 讓程式設計師 們能很方便的做出執行於單一或多重處理器環境下同時執行的程式。不需要考慮處理器 多寡的問題,而直接得到多重處理的效能(如果有多的處理器的話)。此外即使在單一 CPU 的環境下, 如果程式是屬於常常『休息』的那種,如 file 及 socket I/O, thread 還是能提供效能上的增進。 以下將介紹一些簡單的 POSIX thread ,和他在 DEC OSF/1 OS, V3.0.上的版本 (譯註:我是在 solaris 2.5.1 /和 SunOS 4.1.4上測試的啦!差不多。),POSIX thread 簡稱為pthread,他和 non-POSIX 的 cthread非常相近。 2.Hello World 廢話少說,現在就開始吧! pthread_create 函數建立一個 thread 。他需要四個 參數: thread 變數、 thread 特性、一個描述 thread 行為的函數和這個函數所需的 參數。舉例如下: pthread_t a_thread; pthread_attr_t a_thread_attribute; void thread_function(void *argument); char *some_argument; pthread_create( &a_thread, a_thread_attribute, (void *)&thread_function, (void *) &some_argument); thread attribute 描述 thread 的一些特性,目前我們只需要用他來指定 thread 至少需要多大的堆疊。在未來會有許多有趣的 thread attribute ,但就目前而言,大 部分的程式只需簡單的指定 pthread_attr_default 就可以了。不像 process ,需要 使用 fork() system call 讓 child process 和他的 parents 同時開始執行, thread 從 pthread_create 中指定的 thread_function 開始執行。理由非常簡單:如果 thread 不從一個另外的地方開始執行,將會造成一堆 thread 使用相同的系統資源執 行相同的指令。記得嗎? thread 是『共享』系統資源的。(譯註:在這裡停下來,回 憶一下 process 是怎麼產生的... ^_^) 在知道如何產生一個 thread 後,就可以開始我們的第一個 thread 程式了!來設 計一個 multi-thread 版的 printf("Hello world\n"); 吧!一開始,我們需要兩個 thread 變數,和一個 thread function ,另外,還要能告訴不同的 thread 印出不同 的訊息。我想要讓不同的 thread 印出 "hello world" ,不同的兩個部分 "hello" 和 "world"。程式看起來像這樣: void print_message_function( void *ptr ); main() { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "World"; pthread_create( &thread1, pthread_attr_default, (void*)&print_message_function, (void*) message1); pthread_create(&thread2, pthread_attr_default, (void*)&print_message_function, (void*) message2); exit(0); } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s ", message); } 注意 pthread_create 的參數 print_message_function 和他的參數 message1、 message2,這支程式用 pthread_create 產生第一個 thread ,並以 "Hello" 作為其 起始參數;接下來產生第二個 thread ,指定其起始參數為 "World" 。第一個 thread 開始啟動的時候,從 print_message_function 開始執行,其傳入參數為 "Helllo" 。 他將 "Hello" 印出來,然後結束。第二個 thread 則做差不多的事情:印出 "World" 。看起來很合理,但是這個程式有兩個主要的缺陷。 第一個缺點,由於兩個 thread 是同時進行的,所以我們無法保證第一個thread 會先執行到 printf 那一行,所以在螢幕上可能會看到 "Hello World" ,也有可能會 看到 "World Hello"。另外,在 main(parent thread)裡的 exit 呼叫將結束整個 process ,這將導致所有的 thread 一起結束。所以說,如果 exit 在 printf 前被執 行的話,將不會有任何的輸出產生。事實上,在任何一個 thread (不論 parent or child)裡呼叫 exit 都將導致 process 結束,而所有的 thread 也跟著一起結束了。 所以如果要結束一個 thread ,我們必須使用 pthread_exit 這個函數。 在我們小程式裡有兩個競爭條件(race condition),一、看看是 parent process 先執行到 exit 呢?還是 child process 先執行到 printf ?二、還有兩個child thread 到底是誰會先印出訊息呢?為了讓程式按照我們希望的順序運作,我們嘗試強 迫每個 thread 間相互的等待,下面這個程式加入了兩個 sleep 達成這個目的。 void print_message_function( void *ptr ); main() { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "World"; pthread_create( &thread1, pthread_attr_default, (void *) &print_message_function, (void *) message1); sleep(10); //休息一下,等"Hello"印出來再產生下一個 thread pthread_create(&thread2, pthread_attr_default, (void *) &print_message_function, (void *) message2); sleep(10); //休息一下,等"World"印出來再結束。 exit(0); } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s", message); pthread_exit(0); } 這個程式達成我們的目的了嗎?不完全是,原因在於使用 timming delay 來達成 thread 間的同步是錯誤的,因為 thread 間的緊密耦合(tightly coupled)特性很 容易讓我們使用一些不精確的方法來達成其間的同步處理;然而我們卻不該這麼做。 在這個程式中我們遇到的競爭條件和分散式應用程式中,資源共享的情況完全相同。 共享的資源為標準輸出,而分散計算成原則為程式中的三個 thread。第一個thread 必須在第二個thread 前使用 printf/stdout,而兩者皆必須在 parent thread 呼叫 exit 前完成他們的工作。 除了使用 delay 來達成同步的效果外,另一個錯誤發生在 sleep 系統呼叫;如同 exit 對 process 的影響一樣,當 thread 呼叫 sleep 時,講導致整個 process 停下 來。這表示所有屬於這個 process 的 thread 也將跟著停頓下來。因此在上面這個程 式中,呼叫 sleep 除了平白讓程式慢了20秒,並不會有什麼額外影響。另外一個適用 的函數是 pthread_delay_np (np 表示 not process)。舉例來說,要讓thread 停頓 兩秒鐘,可以用下列程式: struct timespec delay; delay.tv_sec = 2; delay.tv_nsec = 0; pthread_delay_np( &delay ); 本節提到的函數有:pthread_create(), pthread_exit(), pthread_delay_np(). 3.Thread 同步問題 POSIX 提供了兩組用來使 thread 同步的基本指令: mutex 和 condition variable。mutex 指的是一組用來控制共享資源存取的一組函數。注意,在使用thread 的情況下,因為整個位址空間都是共用的,所以所有的東西都可以視為共享資源。在 一般情況下, thread 使用一些在pthreadcreate 之前定義或在其所呼叫的函數中定 義的變數來完成其工作,並將他的成果經由整體變數合併。對這些大家都可以存取的 變數,我們必須加以控制。 以下是一個 reader/writer 程式,程式中有一個reader,一個writer,他們共享 一個 buffer,且使用 mutex 來控制這個 buffer 的存取。 void reader_function(void); void writer_function(void); char buffer; int buffer_has_item = 0; pthread_mutex_t mutex; struct timespec delay; main() { pthread_t reader; delay.tv_sec = 2; delay.tv_nsec = 0; pthread_mutex_init(&mutex, pthread_mutexattr_default); pthread_create( &reader, pthread_attr_default, (void*)&reader_function, NULL); writer_function(); } void writer_function(void) { while(1) { pthread_mutex_lock( &mutex ); if ( buffer_has_item == 0 ) { buffer = make_new_item(); buffer_has_item = 1; } pthread_mutex_unlock( &mutex ); pthread_delay_np( &delay ); } } void reader_function(void) { while(1) { pthread_mutex_lock( &mutex ); if ( buffer_has_item == 1) { consume_item( buffer ); buffer_has_item = 0; } pthread_mutex_unlock( &mutex ); pthread_delay_np( &delay ); } } 在這個簡單的程式中,我們假設 buffer 的容量只有 1,因此這個 buffer 有兩 個可能的狀態:『有一筆資料』或『沒有資料』。 writer 首先將 mutex 鎖定,如 果 mutex 已經被鎖定,則暫停,直到 mutex 被解鎖。然後看看 buffer 是否是空的, 若buffer 處於『沒有資料』的狀態,writer 產生一筆新的資料,將其放入 buffer 中。然後將旗標 buffer_has_item 設為 1,讓 reader 可藉此旗標得知 buffer 內有 一筆資料。最後 writer 將 mutex 解鎖,並休息 2 秒鐘,讓 reader 可藉此一空檔 取出 buffer 內的資料。這裡使用的 delay跟之前的 delay 有截然不同的意義,如果 不加上這個 delay 的話,writer 在 unlock mutex 後的下一個指令就是為了產生另 一筆新的資料,再度 lock mutex。這將造成 reader 沒有機會讀取 buffer 中的資料 。因此在此處加上一個 delay 看起來是個不錯的主意。 reader 看起來和 writer 差不多,它首先 lock mutex,然後看看buffer 中是否 有資料,若有資料則將其取出,然後將 mutex 解鎖,接著 delay 2 秒,讓 writer 有 機會放入新的資料。在這個例子中,writer 和 reader 就這樣一直的 run 下去,不斷 的產生/移除 buffer 中的資料。在其他的情況下,我們可能不再需要使用 mutex 了, 此時可以使用 pthread_mutex_destroy(&mutex); 來釋放 mutex。 在初始 mutex 的時候,我們使用了 pthread_mutexattr_default 來當作 mutex 特性。在 OSF/1 中,mutex 特性沒啥用處,所以這樣設就夠了。 mutex 一般用在解決 race condition 問題,但是 mutex 並不是一個很強的機制 ,因為他只有兩個狀態:locked 和 unlocked。POSIX 定義的條件變數(condition variable)將 mutex 的功能加以延伸,能夠做到讓某一個 thread 能暫停,並等待另 一個 thread 的信號(signal)。當信號來了,thread 就醒過來,然後將相關的 mutex lock 起來。這樣的作法可以解決 reader/writer 程式中的 spin-lock 問題。 附錄 A 中有一個使用 mutex 和 condition variable 做成的一個簡單的 integer semaphores。有關 condition variable 的詳細用法可以參考 man page。 本節提到的函數有:pthread_mutex_init(), pthread_mutex_lock(), pthread_mutex_unlock(), pthread_mutex_destroy(). 4. 使用 Semaphores 達成協調工作 (本節中用的Semapore 函數怪怪的,一般我不是這樣用。看起來如果要這 樣寫,必須用附錄 中的 library。) 接下來我們想要用 semaphore 來重寫上節之 reader/writer 程式。用更強悍的 整數 semaphore 來取代 mutex ,並解決 spin-lock 問題。與 Semaphore 相關的運 算有 semaphore_up,semaphore_down,semaphore_init, semaphore_destroy, 和 semaphore_decrement. 其中 semaphore_up 和 semaphore_down 和傳統的 semaphore 語法相同 -- down 運算將在 semaphore 之值小於或等於零時暫停。而 up 運算則遞 增 semaphore。 在使用 semaphore 前必須呼叫 init 函數,而所有 semaphore 的 初始值均為 1。當 semaphore 不再被使用時, destroy 函數可以釋放它。上述所有 函數都只需要一個參數:一個指向 semaphore 物件的指標。 Semaphore_decrement 是一個 non-blocking function 他可以將 semaphore 遞 減到一個負值,這個作法有什麼用處呢?一般用於在初始一個 semaphore 時設定它的 初始值。稍後我們會舉出一個例子。接下來首先看 semaphore 版本的 reader/writer 程式。 void reader_function(void); void writer_function(void); char buffer; Semaphore writers_turn; Semaphore readers_turn; main() { pthread_t reader; semaphore_init( &readers_turn ); semaphore_init( &writers_turn ); /* writer must go first */ semaphore_down( &readers_turn ); pthread_create( &reader, pthread_attr_default, (void *)&reader_function, NULL); writer_function(); } void writer_function(void) { while(1) { semaphore_down( &writers_turn ); buffer = make_new_item(); semaphore_up( &readers_turn ); } } void reader_function(void) { while(1) { semaphore_down( &readers_turn ); consume_item( buffer ); semaphore_up( &writers_turn ); } } 上面這個例子尚未完前展現 integer semaphore 的威力。接下來我們將修改第二 節中的 Hello World 程式,並使用 semaphore 來修正其 race conditions 問題。 void print_message_function( void *ptr ); Semaphore child_counter; Semaphore worlds_turn; main() { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "World"; semaphore_init( &child_counter ); semaphore_init( &worlds_turn ); semaphore_down( &worlds_turn ); /* world goes second */ semaphore_decrement( &child_counter ); /* value now 0 */ semaphore_decrement( &child_counter ); /* value now -1 */ /* * child_counter now must be up-ed 2 times for a thread blocked on it * to be released * */ pthread_create( &thread1, pthread_attr_default, (void *) &print_message_function, (void *) message1); semaphore_down( &worlds_turn ); pthread_create(&thread2, pthread_attr_default, (void *) &print_message_function, (void *) message2); semaphore_down( &child_counter ); /* not really necessary to destroy since we are exiting anyway */ semaphore_destroy ( &child_counter ); semaphore_destroy ( &worlds_turn ); exit(0); } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s ", message); fflush(stdout); semaphore_up( &worlds_turn ); semaphore_up( &child_counter ); pthread_exit(0); } 很容易可以看出,上面這個程式並沒有race condition 問題,而且也會依照正確 的順序印出結果。其中 semaphore child_counter 的目的在於讓 parent thread 暫停 ,直到所有的 children 執行 printf 和緊隨其後的 semaphore_up(&child_counter) 。 本節提到的函數有:semaphore_init(), semaphore_up(), semaphore_down(), semaphore_destroy(), semaphore_decrement(). 5.使用實務 Compile 使用 pthread 的程式,必須 include 相關的header file(譯註:一般 是 pthread.h)並且連結 pthread library: cc hello_world.c -o hello_world -lpthreads (在 Alpha 上你還要加上 -lc_r) (譯註:在 solaris 上用 -lthread 或 -lpthread 都可以。) 如果要使用 semaphore 則還必須使用相關的 header file 和 library。 DEC 的 pthread 是根據 POSIX IV 的 thread 標準而非 POSIX VIII 發展出來的。 函數 pthread_join 允許一個 thread 等待另一指定的 thread 到該 thread 結束。因 此在 Hello World 程式中,可以用來判斷 children thread 是否結束。但是在 DEC 上,這個函數不太可靠,在下列程式段中,如果指定的 some_thread 不存在,他將會 造成錯誤,而不是直接 return。 pthread_t some_thread; void *exit_status; pthread_join( some_thread, &exit_status ); 另外一些奇怪的錯誤可能發生在 thread 函數之外的地方,但是卻肇因於此。 在我們的例子中,並不太去檢查 thread 函數是否正確執行,然而這卻是必要的。幾乎 所有的 pthread 函數都在發生錯誤時 return -1。舉例如下: pthread_t some_thread; if ( pthread_create( &some_thread, ... ) == -1 ) { perror("Thread creation error"); exit(1); } semaphore library 在發生錯誤的時候會印出一些訊息然後離開。 文中沒有舉出來,但是蠻有用的一些函數如下。 pthread_yield(); 通知 scheduler thread 想要出讓他的執行權力, 不需要參數。 pthread_t me; me = pthread_self(); 讓 thread 取得他自己的 identifier。 pthread_t thread; pthread_detach(thread); 通知 library 在後面的pthread_join 呼叫裡,不需 exit status,可增進 thread 的效率。 Appendix A - Semaphore Library Code ============================================================================== Semaphore.h follows ============================================================================== /****************************************************************************\ * * Written by * Tom Wagner (wagner@cs.umass.edu) * at the Distributed Problem Solving Lab * Department of Computer Science, University of Massachusetts, * Amherst, MA 01003 * * Copyright (c) 1995 UMASS CS Dept. All rights are reserved. * * Development of this code was partially supported by: * ONR grant N00014-92-J-1450 * NSF contract CDA-8922572 * * --------------------------------------------------------------------------- * * This code is free software; you can redistribute it and/or modify it. * However, this header must remain intact and unchanged. Additional * information may be appended after this header. Publications based on * this code must also include an appropriate reference. * * This code is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. * \****************************************************************************/ #ifndef SEMAPHORES #define SEMAPHORES #include #include typedef struct Semaphore { int v; pthread_mutex_t mutex; pthread_cond_t cond; } Semaphore; int semaphore_down (Semaphore * s); int semaphore_decrement (Semaphore * s); int semaphore_up (Semaphore * s); void semaphore_destroy (Semaphore * s); void semaphore_init (Semaphore * s); int semaphore_value (Semaphore * s); int tw_pthread_cond_signal (pthread_cond_t * c); int tw_pthread_cond_wait (pthread_cond_t * c, pthread_mutex_t * m); int tw_pthread_mutex_unlock (pthread_mutex_t * m); int tw_pthread_mutex_lock (pthread_mutex_t * m); void do_error (char *msg); #endif ============================================================================== Semaphore.c follows ============================================================================== /****************************************************************************\ * * Written by * Tom Wagner (wagner@cs.umass.edu) * at the Distributed Problem Solving Lab * Department of Computer Science, University of Massachusetts, * Amherst, MA 01003 * * Copyright (c) 1995 UMASS CS Dept. All rights are reserved. * * Development of this code was partially supported by: * ONR grant N00014-92-J-1450 * NSF contract CDA-8922572 * * --------------------------------------------------------------------------- * * This code is free software; you can redistribute it and/or modify it. * However, this header must remain intact and unchanged. Additional * information may be appended after this header. Publications based on * this code must also include an appropriate reference. * * This code is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. * \****************************************************************************/ #include "semaphore.h" /* * function must be called prior to semaphore use. * */ void semaphore_init (Semaphore * s) { s->v = 1; if (pthread_mutex_init (&(s->mutex), pthread_mutexattr_default) == -1) do_error ("Error setting up semaphore mutex"); if (pthread_cond_init (&(s->cond), pthread_condattr_default) == -1) do_error ("Error setting up semaphore condition signal"); } /* * function should be called when there is no longer a need for * the semaphore. * */ void semaphore_destroy (Semaphore * s) { if (pthread_mutex_destroy (&(s->mutex)) == -1) do_error ("Error destroying semaphore mutex"); if (pthread_cond_destroy (&(s->cond)) == -1) do_error ("Error destroying semaphore condition signal"); } /* * function increments the semaphore and signals any threads that * are blocked waiting a change in the semaphore. * */ int semaphore_up (Semaphore * s) { int value_after_op; tw_pthread_mutex_lock (&(s->mutex)); (s->v)++; value_after_op = s->v; tw_pthread_mutex_unlock (&(s->mutex)); tw_pthread_cond_signal (&(s->cond)); return (value_after_op); } /* * function decrements the semaphore and blocks if the semaphore is * <= 0 until another thread signals a change. * */ int semaphore_down (Semaphore * s) { int value_after_op; tw_pthread_mutex_lock (&(s->mutex)); while (s->v <= 0) { tw_pthread_cond_wait (&(s->cond), &(s->mutex)); } (s->v)--; value_after_op = s->v; tw_pthread_mutex_unlock (&(s->mutex)); return (value_after_op); } /* * function does NOT block but simply decrements the semaphore. * should not be used instead of down -- only for programs where * multiple threads must up on a semaphore before another thread * can go down, i.e., allows programmer to set the semaphore to * a negative value prior to using it for synchronization. * */ int semaphore_decrement (Semaphore * s) { int value_after_op; tw_pthread_mutex_lock (&(s->mutex)); s->v--; value_after_op = s->v; tw_pthread_mutex_unlock (&(s->mutex)); return (value_after_op); } /* * function returns the value of the semaphore at the time the * critical section is accessed. obviously the value is not guarenteed * after the function unlocks the critical section. provided only * for casual debugging, a better approach is for the programmar to * protect one semaphore with another and then check its value. * an alternative is to simply record the value returned by semaphore_up * or semaphore_down. * */ int semaphore_value (Semaphore * s) { /* not for sync */ int value_after_op; tw_pthread_mutex_lock (&(s->mutex)); value_after_op = s->v; tw_pthread_mutex_unlock (&(s->mutex)); return (value_after_op); } /* -------------------------------------------------------------------- */ /* The following functions replace standard library functions in that */ /* they exit on any error returned from the system calls. Saves us */ /* from having to check each and every call above. */ /* -------------------------------------------------------------------- */ int tw_pthread_mutex_unlock (pthread_mutex_t * m) { int return_value; if ((return_value = pthread_mutex_unlock (m)) == -1) do_error ("pthread_mutex_unlock"); return (return_value); } int tw_pthread_mutex_lock (pthread_mutex_t * m) { int return_value; if ((return_value = pthread_mutex_lock (m)) == -1) do_error ("pthread_mutex_lock"); return (return_value); } int tw_pthread_cond_wait (pthread_cond_t * c, pthread_mutex_t * m) { int return_value; if ((return_value = pthread_cond_wait (c, m)) == -1) do_error ("pthread_cond_wait"); return (return_value); } int tw_pthread_cond_signal (pthread_cond_t * c) { int return_value; if ((return_value = pthread_cond_signal (c)) == -1) do_error ("pthread_cond_signal"); return (return_value); } /* * function just prints an error message and exits * */ void do_error (char *msg) { perror (msg); exit (1); } -- 我不忍心稱他們為籠中鳥, 因為我自己也不是籠外的鳥, 再怎麼振翅高飛,也飛不出這個地球。 —聖嚴法師— -- * Origin: ★ 交通大學資訊科學系 BBS ★ -- Origin: 從零開始 freebsd.ee.ntu.edu.tw (140.112.19.123)