POSIX Threads 是一套符合 POSIX 標準的 API,方便開發者設計出 User-level 的多執行緒程式。
先了解執行緒的記憶體分配有助於多執行緒程式的開發。
在同一個 Program 中,多個 Thread 會共用同一個位址空間,每個 Thread 都會分配到一塊空間作為自己的 Stack,而指向這些空間起始點的指標就被稱為 Stack pointer
。
呼叫函式和一般的跳躍不同,在呼叫結束後必須回到原本呼叫的地方,原本執行中的位址被叫做「回傳位址」(return address)。如果說呼叫只會發生一次的話,隨便找一個暫存器存回傳位址就好了;但是函式呼叫可以一層一層呼叫下去,所以必須把回傳位址存在記憶體裡。實務上,回傳位址被存在記憶體中的堆疊(stack)裡。
堆疊,被實作成只能使用堆疊空間最上方位址所存的一個變數。而這個紀錄堆疊最上方的紀錄空間被稱為「堆疊指標」(stack pointer)。x86-64 中,為了方便寫呼叫函式的程式,提供了堆疊指標專用的暫存器,和使用這個暫存器的指令。往堆疊上堆資料的操作是「push」,而取出堆疊資料的操作是「pop」。
-- C編譯器入門~想懂低階系統從自幹編譯器開始~
當執行緒呼叫其他函式時,stack pointer 便會向下移動,這讓我們可以有更多空間去存放參數以及局部變數。
當函式執行完畢並返回時,stack pointer 便會移動到原先的位址。
舊的 stack pointer 紀錄的地址也會被存放在 Stack 中,這也是函式可以快速返回的原因。
對於函式的流程控制,這部The Call Stack影片有詳細的解說。
Pthreads API 中大致共有 100 個函式呼叫,全都以 pthread_ 開頭,並可以分為四類:
POSIX 的 Semaphore API 可以與 POSIX threads 一同運作,但 Semaphore API 並非 threads standard 的一部分,其定義在 POSIX.1b, Real-time extensions (IEEE Std 1003.1b-1993) standard 內。
而本篇文章要介紹的是第一項: 執行緒管理的部分。
我們可以利用 POSIX Thread 建立具有一個執行緒以上的 Process,第一個 Thread 會負責運行 main()
中的程式碼。若要建立一個以上的執行緒,我們可以使用 pthread_create
:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
其中 void *(*start_routine) (void *)
用語言表達的話,可以解釋成:
一個指標它帶有一個指向 void 型態資料的指標,並且,它會返回指向 void 型態資料的指標。
如果仍無法理解上述的程式碼,建議讀者可以去複習重拾 C 語言::函式指標。
看完 posix_create
的定義以後,可以看看以下範例:
#include <stdio.h>
#include <pthread.h>
void *busy(void *ptr) {
// ptr will point to "Hi"
puts("Hello World");
return NULL;
}
int main() {
pthread_t id;
pthread_create(&id, NULL, busy, "Hi");
while (1) {} // Loop forever
}
如果要等待我們建立的執行緒完成工作,需要使用 pthread_join
:
int pthread_join(pthread_t thread, void **retval);
查看定義後,進一步改寫原本的程式碼:
#include <stdio.h>
#include <pthread.h>
void *busy(void *ptr) {
// ptr will point to "Hi"
puts("Hello World");
return NULL;
}
int main() {
void *result;
pthread_t id;
pthread_create(&id, NULL, busy, "Hi");
pthread_join(id, &result);
}
除了上面的範例,我們可以用 pthread_exit()
再做一次改寫:
#include <stdio.h>
#include <pthread.h>
void *busy(void *ptr) {
// ptr will point to "Hi"
puts("Hello World");
pthread_exit(NULL);
}
int main() {
pthread_t id;
pthread_create(&id, NULL, busy, "Hi");
pthread_join(id, NULL);
}
若工作流程用圖表呈現,大概是這樣:
上圖取自該網站。
感謝高魁良前輩的補充,使用
-lpthread
僅連結 pthread library,而不會對 thread 的編譯設定進行最佳化,導致pre-defined macros
會無法使用。
本系列都是採用 gcc 作為 C 語言的編譯器,若使用到 Pthread 必須在編譯時添加參數: -pthread
。
gcc source.c -pthread -o source
編譯完成後,便可以啟動可執行檔。
./source
PThread 提供了 API,讓我們可以取消已建立的 POSIX Thread。
int pthread_cancel(pthread_t thread);
想知道更多細節可以參考該連結。
pthread_exit()
如果放在 main()
函式,是用來確保所有用 POSIX Thread API 建立的執行緒已經完成。
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, myfunc, "Jabberwocky");
pthread_create(&tid2, NULL, myfunc, "Vorpel");
pthread_exit(NULL);
// No code is run after pthread_exit
// However process will continue to exist until both threads have finished
}
如果不使用 pthread_exit()
或是 pthread_join()
而直接使用 exit()
,Process 會在一派發完執行緒後結束 (也就是執行緒根本還沒開始處理任務):
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, myfunc, "Jabberwocky");
pthread_create(&tid2, NULL, myfunc, "Vorpel");
exit(42); //or return 42;
// No code is run after exit
}
如果還有疑問,也可以參考 stackoverflow 上的問答串。
最後,筆者統整一下本篇介紹的 POSIX Thread API 的重要知識點:
終止 Thread 有 4 個方法:
pthread_cancel
呼叫指定的執行緒。pthread_exit()
。pthread_join
會有什麼後果呢?空閒的執行緒會繼續占用資源,直到 Process 結束為止。
換言之,如果是在長期不會結束的應用(像是伺服器),那錯誤的設計便會造成多餘的資源浪費。
pthread_join()
還是 pthread_exit()
阿?答案是都可以,只是差在 pthread_exit()
會在執行緒完成任務後退出,讓你沒有機會執行其他程式。
可以,但要注意函式的生命週期,考慮以下程式碼:
pthread_t start_threads() {
int start = 42;
pthread_t tid;
pthread_create(&tid, 0, myfunc, &start); // ERROR!
return tid;
}
等到 myfunc
開始執行時,start_threads()
的生命早就走到盡頭了!這樣一來,我們根本無法確定原先存放 start 變數內容的記憶體現在存放什麼東西。
為了避免這個情況發生,我們可以用 pthread_join
改寫範例程式:
void start_threads() {
int start = 42;
void *result;
pthread_t tid;
pthread_create(&tid, 0, myfunc, &start); // OK - start will be valid!
pthread_join(tid, &result);
}
這樣一來,start_thread()
的生命週期就會被延後到 myfunc()
執行完成才結束。