# Pthreads,第 2 部分:實踐中的用法
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Pthreads%2C-Part-2%3A-Usage-in-Practice>
## 更多 pthread 功能
## 如何創建 pthread?
參見 [Pthreads 第 1 部分](https://github.com/angrave/SystemProgramming/wiki/Pthreads,-Part-1:-Introduction),其中介紹了`pthread_create`和`pthread_join`
## 如果我兩次調用`pthread_create`,我的進程有多少棧?
您的進程將包含三個棧 - 每個線程一個棧。第一個線程是在進程啟動時創建的,您還創建了另外兩個。實際上可以有更多的棧,但是現在讓我們忽略這種復雜性。重要的想法是每個線程都需要一個棧,因為棧包含自動變量和舊的 CPU PC 寄存器,因此它可以在函數完成后返回執行調用函數。
## 完整進程和線程之間有什么區別?
此外,與進程不同,同一進程中的線程可以共享相同的全局內存(數據和堆段)。
## `pthread_cancel`有什么作用?
停止一個帖子。請注意,線程可能實際上不會立即停止。例如,它可以在線程進行操作系統調用時終止(例如`write`)。
在實踐中,`pthread_cancel`很少使用,因為它不會給線程提供自我清理的機會(例如,它可能已經打開了一些文件)。另一種實現是使用 boolean(int)變量,其值用于通知其他線程它們應該完成并清理。
## `exit`和`pthread_exit`有什么區別?
`exit(42)`退出整個過程并設置進程退出值。這相當于 main 方法中的`return 42`。進程內的所有線程都將停止。
`pthread_exit(void *)`僅停止調用線程,即調用`pthread_exit`后線程永不返回。如果沒有其他線程在運行,pthread 庫將自動完成該過程。 `pthread_exit(...)`相當于從線程的函數返回;兩者都完成線程并設置線程的返回值(void *指針)。
在`main`線程中調用`pthread_exit`是簡單程序確保所有線程完成的常用方法。例如,在以下程序中,`myfunc`線程可能沒有時間開始。
```c
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
}
```
接下來的兩個程序將等待新線程完成 -
```c
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
}
```
或者,我們在從 main(或 call exit)返回之前加入每個線程(即等待它完成)。
```c
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, myfunc, "Jabberwocky");
pthread_create(&tid2, NULL, myfunc, "Vorpel");
// wait for both threads to finish :
void* result;
pthread_join(tid1, &result);
pthread_join(tid2, &result);
return 42;
}
```
注意 pthread_exit 版本會創建線程僵尸,但這不是一個長時間運行的進程,所以我們不在乎。
## 如何終止一個線程?
* 從線程函數返回
* 致電`pthread_exit`
* 用`pthread_cancel`取消線程
* 終止進程(例如 SIGTERM);出口();從`main`返回
## pthread_join 的目的是什么?
* 等待線程完成
* 清理線程資源
* 獲取線程的返回值
## 如果你不打電話給`pthread_join`會怎么樣?
完成的線程將繼續消耗資源。最終,如果創建了足夠的線程,`pthread_create`將失敗。實際上,這只是長時間運行進程的一個問題,但對于簡單,短暫的進程來說不是問題,因為當進程退出時,所有線程資源都會自動釋放。
## 我應該使用`pthread_exit`還是`pthread_join`?
`pthread_exit`和`pthread_join`都會讓其他線程自己完成(即使在主線程中調用)。但是,當指定的線程完成時,只有`pthread_join`會返回給您。 `pthread_exit`不會等待并立即結束你的線程并且沒有機會繼續執行。
## 你可以將指針從一個線程傳遞到另一個線程嗎?
是。但是,您需要非常小心棧變量的生命周期。
```
pthread_t start_threads() {
int start = 42;
pthread_t tid;
pthread_create(&tid, 0, myfunc, &start); // ERROR!
return tid;
}
```
上面的代碼無效,因為函數`start_threads`可能會在`myfunc`開始之前返回。該函數傳遞`start`的地址,但是在`myfunc`執行時,`start`不再在范圍內,其地址將重新用于另一個變量。
以下代碼有效,因為棧變量的生命周期比后臺線程長。
```
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);
}
```
## 比賽條件簡介
## 如何創建具有不同起始值的十個線程。
以下代碼應該啟動十個線程,其值為 0,1,2,3,... 9 然而,當運行時打印出`1 7 8 8 8 8 8 8 8 10`!你能明白為什么嗎?
```c
#include <pthread.h>
void* myfunc(void* ptr) {
int i = *((int *) ptr);
printf("%d ", i);
return NULL;
}
int main() {
// Each thread gets a different value of i to process
int i;
pthread_t tid;
for(i =0; i < 10; i++) {
pthread_create(&tid, NULL, myfunc, &i); // ERROR
}
pthread_exit(NULL);
}
```
上面的代碼遭受`race condition` - i 的值正在改變。新線程稍后啟動(在示例輸出中,最后一個線程在循環結束后啟動)。
為了克服這種競爭條件,我們將為每個線程指定一個指向它自己的數據區域的指針。例如,對于每個線程,我們可能希望存儲 id,起始值和輸出值:
```c
struct T {
pthread_t id;
int start;
char result[100];
};
```
這些可以存儲在一個數組中 -
```
struct T *info = calloc(10 , sizeof(struct T)); // reserve enough bytes for ten T structures
```
并且每個數組元素都傳遞給每個線程 -
```
pthread_create(&info[i].id, NULL, func, &info[i]);
```
## 為什么有些功能,例如 asctime,getenv,strtok,strerror 不是線程安全的嗎?
要回答這個問題,讓我們看一個簡單的函數,它也不是“線程安全的”
```c
char *to_message(int num) {
char static result [256];
if (num < 10) sprintf(result, "%d : blah blah" , num);
else strcpy(result, "Unknown");
return result;
}
```
在上面的代碼中,結果緩沖區存儲在全局內存中。這很好 - 我們不希望返回指向棧上無效地址的指針,但整個內存中只有一個結果緩沖區。如果兩個線程同時使用它,那么一個會破壞另一個:
| 時間 | 線程 1 | 線程 2 | 評論 |
| --- | --- | --- | --- |
| 1 | to_m(5) | | |
| 2 | | to_m(99) | 現在兩個線程都會在結果緩沖區中看到“Unknown” |
## 什么是條件變量,信號量,互斥量?
這些是同步鎖,用于防止競爭條件并確保在同一程序中運行的線程之間正確同步。另外,這些鎖在概念上與內核中使用的原語相同。
## 在分叉過程中使用線程有什么好處嗎?
是!在線程之間共享信息很容易,因為(同一進程的)線程存在于同一個虛擬內存空間中。此外,創建線程比創建(分叉)進程要快得多。
## 使用線程而不是分叉過程有什么缺點嗎?
是!沒有隔離!當線程存在于同一進程中時,一個線程可以訪問與其他線程相同的虛擬內存。單個線程可以終止整個過程(例如,通過嘗試讀取地址零)。
## 你能用多個線程分叉一個進程嗎?
是!但是子進程只有一個線程(它是調用`fork`的線程的一個克隆。我們可以看到這是一個簡單的例子,后臺線程永遠不會在子進程中打印出第二條消息。
```c
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
static pid_t child = -2;
void *sleepnprint(void *arg) {
printf("%d:%s starting up...\n", getpid(), (char *) arg);
while (child == -2) {sleep(1);} /* Later we will use condition variables */
printf("%d:%s finishing...\n",getpid(), (char*)arg);
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1,NULL, sleepnprint, "New Thread One");
pthread_create(&tid2,NULL, sleepnprint, "New Thread Two");
child = fork();
printf("%d:%s\n",getpid(), "fork()ing complete");
sleep(3);
printf("%d:%s\n",getpid(), "Main thread finished");
pthread_exit(NULL);
return 0; /* Never executes */
}
```
```
8970:New Thread One starting up...
8970:fork()ing complete
8973:fork()ing complete
8970:New Thread Two starting up...
8970:New Thread Two finishing...
8970:New Thread One finishing...
8970:Main thread finished
8973:Main thread finished
```
實際上,在分叉之前創建線程可能會導致意外錯誤,因為(如上所示)其他線程在分叉時會立即終止。另一個線程可能只是鎖定互斥鎖(例如通過調用 malloc)并且永遠不會再次解鎖它。高級用戶可能會發現`pthread_atfork`有用但我們建議您通常盡量避免在分叉之前創建線程,除非您完全理解此方法的局限性和困難。
## 還有其他原因使`fork`可能比創建線程更可取。
創建單獨的進程很有用
* 需要更高安全性時(例如,Chrome 瀏覽器對不同的標簽使用不同的進程)
* 運行現有的完整程序時,需要一個新的過程(例如,啟動'gcc')
* 當您遇到同步原語并且每個進程正在對系統中的某些操作進行操作時
## 我怎樣才能找到更多?
請參閱[手冊頁](http://man7.org/linux/man-pages/man3/pthread_create.3.html)中的完整示例和 [pthread 參考指南](http://man7.org/linux/man-pages/man7/pthreads.7.html)另外:[簡明的第三方示例代碼解釋創建,加入和退出](http://www.thegeekstuff.com/2012/04/terminate-c-thread/)
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話