iT邦幫忙

2021 iThome 鐵人賽

DAY 27
0
Software Development

微自幹的作業系統輕旅行系列 第 34

POSIX Thread 介紹

POSIX Threads 是一套符合 POSIX 標準的 API,方便開發者設計出 User-level 的多執行緒程式。

開始之前

先了解執行緒的記憶體分配有助於多執行緒程式的開發。

thread 空間分配

在同一個 Program 中,多個 Thread 會共用同一個位址空間,每個 Thread 都會分配到一塊空間作為自己的 Stack,而指向這些空間起始點的指標就被稱為 Stack pointer

呼叫函式和一般的跳躍不同,在呼叫結束後必須回到原本呼叫的地方,原本執行中的位址被叫做「回傳位址」(return address)。如果說呼叫只會發生一次的話,隨便找一個暫存器存回傳位址就好了;但是函式呼叫可以一層一層呼叫下去,所以必須把回傳位址存在記憶體裡。實務上,回傳位址被存在記憶體中的堆疊(stack)裡。
堆疊,被實作成只能使用堆疊空間最上方位址所存的一個變數。而這個紀錄堆疊最上方的紀錄空間被稱為「堆疊指標」(stack pointer)。x86-64 中,為了方便寫呼叫函式的程式,提供了堆疊指標專用的暫存器,和使用這個暫存器的指令。往堆疊上堆資料的操作是「push」,而取出堆疊資料的操作是「pop」。
-- C編譯器入門~想懂低階系統從自幹編譯器開始~

當執行緒呼叫其他函式時,stack pointer 便會向下移動,這讓我們可以有更多空間去存放參數以及局部變數。
當函式執行完畢並返回時,stack pointer 便會移動到原先的位址。

舊的 stack pointer 紀錄的地址也會被存放在 Stack 中,這也是函式可以快速返回的原因。

ref

對於函式的流程控制,這部The Call Stack影片有詳細的解說。

進入正題

Pthreads API 中大致共有 100 個函式呼叫,全都以 pthread_ 開頭,並可以分為四類:

  • 執行緒管理,例如建立執行緒,等待 (join) 執行緒,查詢執行緒狀態等。
  • Mutex lock: 建立、摧毀、鎖定、解鎖、設定屬性等操作
  • Condition Variable: 建立、摧毀、等待、通知、設定與查詢屬性等操作
  • 使用了互斥鎖的執行緒間的同步管理

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);
}

若工作流程用圖表呈現,大概是這樣:

pthread join

上圖取自該網站

Compile your code!

感謝高魁良前輩的補充,使用 -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);

想知道更多細節可以參考該連結

exit 和 pthread_exit 的差異

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

終止 Thread 有 4 個方法:

  • 等到 Thread 指派的任務 Return。
  • pthread_cancel 呼叫指定的執行緒。
  • 使用 pthread_exit()
  • 終止 Process。

如果不使用 pthread_join 會有什麼後果呢?

空閒的執行緒會繼續占用資源,直到 Process 結束為止。
換言之,如果是在長期不會結束的應用(像是伺服器),那錯誤的設計便會造成多餘的資源浪費。

我該用 pthread_join() 還是 pthread_exit() 阿?

答案是都可以,只是差在 pthread_exit() 會在執行緒完成任務後退出,讓你沒有機會執行其他程式。

我可以在執行緒中傳送 Stack pointer 到另一個執行緒嗎?

可以,但要注意函式的生命週期,考慮以下程式碼:

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() 執行完成才結束。

Reference


上一篇
PPT in Operating system
下一篇
並行程式的潛在問題 (一)
系列文
微自幹的作業系統輕旅行39

1 則留言

0
高魁良
iT邦新手 4 級 ‧ 2021-10-12 14:46:25
EN iT邦研究生 3 級 ‧ 2021-10-12 16:05:02 檢舉

感謝提醒,我等等會把差別補上 <(_ _)>

我要留言

立即登入留言