[TOC] 1. 線程與進程 線程的概念 線程是進程內相對獨立的一個執行流,是進程內的一個執行單元,是操作系統中一個可調度的實體。 深入理解進程和線程 在現代操作系統中,資源分配的基本單位是進程,而CPU調度執行的基本單位是線程 進程不是調度單元,線程是進程使用CPU資源的基本單位 進程有獨立的地址 ...
目錄
1. 線程與進程
線程的概念
線程是進程內相對獨立的一個執行流,是進程內的一個執行單元,是操作系統中一個可調度的實體。
深入理解進程和線程
- 在現代操作系統中,資源分配的基本單位是進程,而CPU調度執行的基本單位是線程
- 進程不是調度單元,線程是進程使用CPU資源的基本單位
- 進程有獨立的地址空間,進程中可以存在多個線程共用進程資源
- 線程不能脫離進程單獨存在,只能依附於進程運行
- 線程可以在不影響進程的情況下終止,但反之則不然
2. 多線程
什麼是多線程
多線程,是指從軟體或硬體層面上實現多個線程併發執行的技術。
- 從軟體層面看,一個進程中可以有多個線程,該程式也可以稱之為多線程程式;
- 從硬體層面看,多核處理器能夠支持在同一時間執行多個線程。
實際上,對於單核處理器,即使軟體編寫為多線程模型,同一時間也只能執行一個線程,但這並不代表此時多線程就沒有意義,因為處理器的數量不會影響程式結構,那麼多線程編程模型在程式結構上到底有哪些好處呢?
多線程模型的好處
- 通過為每種事件類型分配單獨的處理線程,可以簡化非同步事件處理代碼
- 可以直接共用進程的數據資源
- 將複雜問題分解為相互獨立的任務,可以交叉進行,提高程式吞吐量
- 通過把處理用戶輸入輸出的部分和其他部分分開,可以改善互動式程式響應時間
3. 線程標識
- 線程ID(Thread ID)是線程的唯一標識
- 線程ID只有在它所屬的進程上下文中才有意義
- 線程ID類型為
pthread_t
,可能實現為unsigned long或結構體,依系統而定 - 線程可以調用
pthread_self
獲得自身線程ID - 可移植的程式應該調用
pthread_equal
來比較兩個線程的ID
#include <pthread.h>
pthread_t pthread_self(); //返回調用線程的線程ID
int pthread_equal(pthread_t tid1, pthread_t tid2); //相等返回非0數值,否則返回0
4. 線程創建
函數原型
任意線程可以通過調用pthread_create創建新線程,start_routine為新線程的啟動常式,創建成功後,新線程和調用線程誰先運行是不確定的。
//成功返回0,失敗返回錯誤編號
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
參數說明
- pthread_create成功返回後,tid指向記憶體即為新線程ID
- attr用於定製線程屬性,若使用預設屬性則傳NULL
- start_routine是線程啟動常式
- arg是start_routine的參數,若參數不止一個,就把這些參數放到一個結構中,再把該結構的地址作為arg傳入
使用示例-列印線程ID
#include <pthread.h>
#include <stdio.h>
pthread_t tid;
void printf_tid(const char *s)
{
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid = %d, tid = %lu(0x%lx)\n", s, pid, tid, tid);
}
void *pthread_start(void *arg)
{
printf_tid("new thread: "); //新線程用pthread_self()獲取自身ID,是因為新線程執行時pthread_create()可能還未返回,tid還未初始化完成
}
int main()
{
pthread_create(&tid, NULL, pthread_start, NULL);
sleep(1); //調用線程休眠1S,讓新線程先執行
printf_tid("main thread: ");
return 0;
}
註意:使用pthread的代碼在編譯時需要指定鏈接-lpthread
5. 線程終止
在不影響整個進程的情況下,單個線程有三種終止方式:
- 線上程啟動常式中調用
return
返回 - 線上程啟動常式中調用
pthread_exit
退出 - 被進程中的其他線程取消
void pthread_exit(void *value_ptr);
value_ptr是一個無類型指針,進程中的其他線程可以調用pthread_join
訪問到這個指針。
6. 線程等待
函數原型
int pthread_join(pthread_t tid, void **value_ptr); //成功返回0,失敗返回錯誤編號
調用線程將一直阻塞,直到等待的線程以上述三種方式終止。
參數說明
- tid表示等待的線程ID
- value_ptr用於保存線程的退出狀態
- 如果線程以return或pthread_exit方式終止,value_ptr指向記憶體就被設置為return或pthread_exit的參數
- 如果線程被取消,value_ptr指向記憶體就被設置為
PTHREAD_CANCELED
- 如果不關心線程的返回值,就給value_ptr傳NULL
使用示例-獲得線程返回值
#include <pthread.h>
#include <stdio.h>
void *thread1_start(void *arg)
{
int ret = 0;
return ((void *)ret);
}
void *thread2_start(void *arg)
{
char *ret = "thread 2 exit";
pthread_exit(ret);
}
int main()
{
pthread_t tid1;
pthread_t tid2;
void *ret;
pthread_create(&tid1, NULL, thread1_start, NULL);
pthread_create(&tid2, NULL, thread2_start, NULL);
pthread_join(tid1, &ret);
printf("thread 1: %d\n", (int)ret);
pthread_join(tid2, &ret);
printf("thread 2: %s\n", (char *)ret);
return 0;
}
7. 線程分離
在預設情況下,線程的終止狀態會一直保存到對該線程調用pthread_join;但是,如果線程已經被分離,其占用的系統資源會線上程終止時被立即回收。
有兩種方式可以使線程分離:
- 調用
pthread_detach
,該函數不會使調用線程阻塞 - 修改線程屬性結構
pthread_attr_t
,以分離狀態創建線程
線上程被分離後,就不能再用pthread_join等待它的終止狀態了,因為對分離狀態的線程調用pthread_join會產生未定義行為。
pthread_detach
int pthread_detach(pthread_t tid); //成功返回0,失敗返回錯誤編號
#include <pthread.h>
#include <stdio.h>
void *thread_start(void *arg)
{
sleep(2);
printf("new thread exit\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_start, NULL);
pthread_detach(tid);
printf("main thread: pthread_detach() return\n");
sleep(5);
return 0;
}
以分離狀態創建線程
/*4個函數的返回值:成功返回0,失敗返回錯誤編號*/
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_destroy(pthread_attr_t *attr);
可以調用pthread_attr_setdetachstate
來設置線程的可分離狀態:
detachstate = PTHREAD_CREATE_DETACHED
,以分離狀態啟動線程detachstate = PTHREAD_CREATE_JOINABLE
,以正常狀態啟動線程
#include <pthread.h>
#include <stdio.h>
void *thread_start(void *arg)
{
sleep(2);
printf("new thread exit\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, thread_start, NULL);
pthread_attr_destroy(&attr);
printf("main thread: pthread_attr_destroy() return\n");
sleep(5);
return 0;
}
8. 線程取消
pthread_cancel
在編寫多線程代碼時,經常面臨線程安全退出問題,一般情況下,最好使用將標誌位置位的方式;
在其他線程中將標誌位置位,然後調用pthread_join等待線程退出,回收線程占用的資源。
void *thread_start(void *arg)
{
while (!quit)
{
//......
}
}
int main()
{
quit = 1;
pthread_join(tid, NULL);
}
但是在某些應用中,線程可能正阻塞於某個函數(如pthread_cond_wait)無法被喚醒,即使設置了標誌位也無法結束。
此時可以在其他線程中調用pthread_cancel請求取消線程,然後立即調用pthread_join等待線程退出。
int pthread_cancel(pthread_t tid); //成功返回0,失敗返回錯誤編號
tid為要取消的線程ID,需要註意的是,pthread_cancel並不等待線程終止,它僅僅是發出請求。
#include <pthread.h>
#include <stdio.h>
void *thread1_start(void *arg)
{
sleep(10);
pthread_exit(NULL);
}
void *thread2_start(void *arg)
{
sleep(10);
pthread_exit(NULL);
}
int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_create(&tid1, NULL, thread1_start, NULL);
pthread_create(&tid2, NULL, thread2_start, NULL);
sleep(1);
pthread_cancel(tid1);
pthread_join(tid1, NULL);
printf("thread 1 exit\n");
sleep(1);
pthread_cancel(tid2);
pthread_join(tid2, NULL);
printf("thread 2 exit\n");
return 0;
}
線程取消屬性
線程取消有兩個屬性,分別是可取消狀態和可取消類型,這兩個屬性不在pthread_attr_t結構中,但它們影響著線程在響應取消請求時的行為。
/*
* 可取消狀態:PTHREAD_CANCEL_ENABLE(允許取消,預設屬性),PTHREAD_CANCEL_DISABLE(不允許取消,但取消請求不會丟失,而是一直處於掛起狀態)
* 可取消類型:PTHREAD_CANCEL_DEFERRED(延遲取消,到達取消點才取消,預設屬性),PTHREAD_CANCEL_ASYNCHRONOUS(非同步取消,可在任意時刻取消)
*/
int pthread_setcancelstate(int state, int *oldstate); //將線程可取消狀態設為state,原有可取消狀態通過oldstate返回,這兩步是原子操作
int pthread_setcanceltype(int type, int *oldtype); //將線程可取消類型設為type,原有可取消類型通過oldtype返回
取消點
預設情況下,線程的可取消類型為延遲取消,也就是說:被取消的線程在取消請求發出後還是繼續運行,直到到達某個取消點。
取消點是線程檢查它是否被取消的一個位置,根據《UNIX環境高級編程 第3版》P362-P363描述,POSIX.1定義的取消點和可選取消點如下。
自定義取消點
如果線程在很長一段時間內都不會調用前面兩張圖中的取消點函數,那麼可以調用pthread_testcancel
線上程中添加自己的取消點。
調用pthread_testcancel時,如果有某個取消請求處於掛起狀態,且可取消狀態為ENABLE,那麼線程就會被取消。
void pthread_testcancel();
使用線程取消的風險
當線程響應取消請求而終止時,主要面臨的兩大風險:
- 線程裡面的鎖可能沒有unlock,有可能導致死鎖
- 線程申請的資源(如堆記憶體)沒有釋放
下麵是一段由pthread_cancel引起的死鎖範例代碼。
#include <pthread.h>
#include <stdio.h>
static pthread_cond_t cond;
static pthread_mutex_t mutex;
void *thread0(void *arg)
{
pthread_mutex_lock(&mutex);
printf("thread 0 lock sucess\n");
pthread_cond_wait(&cond, &mutex); //主線程發出取消請求時,thread1阻塞於slepp(2),thread0阻塞於此取消點,導致thread0未解鎖mutex就終止
printf("thread 0 pthread_cond_wait return\n");
pthread_mutex_unlock(&mutex);
pthread_exit(0);
}
void *thread1(void *arg)
{
sleep(2);
printf("thread 1 start lock\n");
pthread_mutex_lock(&mutex); //thread0終止約1S後,thread1執行到此,由於mutex已加鎖,也沒有其他地方能夠對其解鎖,從而導致死鎖
printf("thread 1 lock sucess\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
pthread_exit(0);
}
int main()
{
pthread_t tid[2];
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid[0], NULL, thread0, NULL);
pthread_create(&tid[1], NULL, thread1, NULL);
sleep(1);
pthread_cancel(tid[0]);
printf("main thread request cancel thread 0\n");
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
線程清理程式
可以使用線程清理程式來解決線程取消的風險問題。線程可以安排它退出時需要調用的函數,這樣的函數稱為線程清理程式。
一個線程可以註冊多個清理程式,處理程式記錄在棧中,也就是說,它們的執行順序和註冊順序是相反的。
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
當線程執行以下動作時,清理程式是由pthread_cleanup_push函數調度的,調用時只有一個參數arg:
- 調用pthread_exit結束線程
- 響應pthread_cancel取消請求
- 用非零execute參數調用pthread_cleanup_pop
註意:如果線程以return方式終止,線程清理程式不會被調用。
不管發生上述哪種情況,pthread_cleanup_pop都將刪除上次pthread_cleanup_push登記的線程清理程式。
這兩個函數有一個限制,由於它們經常實現為巨集,所以必須在與線程啟動常式相同的作用域中以配對的方式使用,否則,可能會產生編譯錯誤。
回到線程取消的風險問題上來,我們只需要線上程清理程式中解鎖和釋放資源,併在線程啟動常式的第一步就註冊清理程式,
這樣,當線程因響應取消請求而終止時,線程清理程式就會得以執行。
#include <pthread.h>
#include <stdio.h>
static pthread_cond_t cond;
static pthread_mutex_t mutex;
void cleanup(void *arg)
{
pthread_mutex_unlock(&mutex);
printf("mutex unlock in cleanup\n");
}
void *thread0(void *arg)
{
pthread_cleanup_push(cleanup, NULL); //註冊線程清理程式進行解鎖
pthread_mutex_lock(&mutex);
printf("thread 0 lock sucess\n");
pthread_cond_wait(&cond, &mutex); //主線程發出取消請求時,thread1阻塞於slepp(2),thread0阻塞於此取消點
printf("thread 0 pthread_cond_wait return\n");
pthread_mutex_unlock(&mutex);
pthread_cleanup_pop(0);
pthread_exit(0);
}
void *thread1(void *arg)
{
sleep(2);
printf("thread 1 start lock\n");
pthread_mutex_lock(&mutex); //thread0終止約1S後,thread1執行到此
printf("thread 1 lock sucess\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
pthread_exit(0);
}
int main()
{
pthread_t tid[2];
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid[0], NULL, thread0, NULL);
pthread_create(&tid[1], NULL, thread1, NULL);
sleep(1);
pthread_cancel(tid[0]);
printf("main thread request cancel thread 0\n");
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
最後,引用一篇由pthread_cancel引起死鎖的博客https://blog.csdn.net/xsckernel/article/details/48052425,提取核心內容如下:
“通常的說法:某某函數是Cancellation Points,這種方法是容易令人混淆的。因為函數的執行是一個時間過程,而不是一個時間點。其實真正的Cancellation Points
只是在這些函數中Cancellation Type被修改為PHREAD_CANCEL_ASYNCHRONOUS和修改回PTHREAD_CANCEL_DEFERRED中間的一段時間。”