
版權(quán)聲明:本文為 cdeveloper 原創(chuàng)文章,可以隨意轉(zhuǎn)載,但必須在明確位置注明出處!
多線程概念
多線程技術(shù)是應用開發(fā)中非常重要的技術(shù)之一,幾乎大型的應用軟件都使用這個技術(shù),這次一起來學習下 Linux 中的多線程開發(fā)基礎(chǔ)(其他的系統(tǒng)中概念也是類似的)。
在 Linux 中,一個簡單的進程可以看成只有一個單線程(主線程),因為只有一個線程,所以進程在某一個時刻只能做一件事。為了能夠使得進程可以在同一時刻做多件事情,可以讓這個進程內(nèi)部產(chǎn)生多個線程來分工同時完成。
例如典型的字處理程序,有一個線程在前臺與用戶進行圖形界面的交互,有一個線程在進行語法和拼寫檢查,還有一個線程在周期性的保存文檔,這 3 個線程共同完成了文檔的編寫和保存功能。想想假如只有一個主線程,那么你先鍵入文檔,然后進行語法和拼寫檢查,最后才保存文檔,這 3 個步驟是串行執(zhí)行的,而使用多線程時這 3 個任務是并行執(zhí)行的,效率提高了很多,也更加安全了,如下圖:

使用多線程技術(shù)有下面幾個優(yōu)點:
- 簡化任務的代碼:在單獨的線程中執(zhí)行一個任務可以使用簡單的同步編程模式
- 共享進程資源:多個線程可以訪問相同的進程地址空間
- 提高進程的吞吐量:使用多線程可以并行運行多個任務
- 優(yōu)化程序體驗:可以使用多線程分開處理程序的輸入輸出
總體來說使用多線程技術(shù)可以優(yōu)化程序,提升用戶的體驗。了解了基本概念后,接下來看看操作系統(tǒng)實現(xiàn)線程的模型。
多線程模型
現(xiàn)在的操作系統(tǒng)有 2 種不同的方法來提供線程支持:
- 用戶線程:受內(nèi)核支持,無須內(nèi)核管理
- 內(nèi)核線程:由內(nèi)核直接支持和管理
這兩種方法之間有一定的聯(lián)系,畢竟用戶線程要受內(nèi)核的支持,有 3 種常見的建立兩者關(guān)系的模型:
- 多對一模型:多個用戶線程映射到一個內(nèi)核線程,多個線程不能并行執(zhí)行
- 一對一模型:一個用戶線程映射到一個內(nèi)核線程,創(chuàng)建線程的開銷較大
- 多對多模型:多個用戶線程可以復用同樣數(shù)量或更小數(shù)量的內(nèi)核線程,沒有前面 2 者的缺點
3 種模型圖如下:

實現(xiàn)的模型在不同的操作系統(tǒng)上都差不多,但是不同操作系統(tǒng)上的線程庫的實現(xiàn)卻是不大相同的。
線程庫
操作系統(tǒng)為我們提供創(chuàng)建和管理線程的 API,線程操作也有 2 種實現(xiàn)方法:
- 內(nèi)核支持的用戶線程庫:此庫的代碼和數(shù)據(jù)都存在用戶空間中,API 不會進行系統(tǒng)調(diào)用
- 原始的內(nèi)核級別的庫:此庫的代碼和數(shù)據(jù)都存在內(nèi)核空間,API 會進行系統(tǒng)調(diào)用
有 3 種主要的線程庫:
-
Posix Pthread:POSIX 標準的拓展,可以提供用戶級和內(nèi)核級的庫,但僅僅是線程行為規(guī)范,而不是實現(xiàn),Linux,Solaris 等 OS 都實現(xiàn)了這個規(guī)范 -
Win 32:適用于 Windows OS 的內(nèi)核級線程庫 -
Java線程:由于 JVM 運行在宿主 OS 上,所以 Java 線程 API 通常采用宿主 OS 上的線程庫的實現(xiàn)
因為本次介紹的是 Linux 下的多線程技術(shù),所以這里學習的就是 Posix Pthread 的規(guī)范定義的 API 了,下面來看看都有哪些常用的函數(shù)。
比較,獲取線程 ID
就像每個進程有一個進程 ID 一樣,每個線程也有一個線程 ID,線程 ID 只有在它所屬的進程上下文中才有意義。在 Posix Pthread 中用 pthread_t 類型來表示一個線程 ID,該類型在 Linux 下是一個無符號長整型,并提供了 2 個相關(guān)的操作:
#include <pthread.h>
// 比較 2 個線程 ID
int pthread_equal(pthread_t t1, pthread_t t2);
// 獲取自身線程 ID
pthread_t pthread_self(void);
這兩個函數(shù)比較簡單,就不介紹例子了,返回值等信息可以參考 man pthread_equal 和 man pthread_self 手冊。
創(chuàng)建線程
Posix 線程定義下面的函數(shù)來創(chuàng)建新的線程:
#include <pthread.h>
/*
* thread: 指向線程 ID
* attr: 定制線程屬性,傳遞 NULL 設置默認屬性
* start_routine: 要運行的線程函數(shù)
* arg: 要傳遞的函數(shù)參數(shù)
* return: 成功返回 0,失敗返回錯誤碼,但不會設置 erron
*/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
來看一個簡單的創(chuàng)建線程的例子,這個例子創(chuàng)建一個子線程并打印子線程的進程 ID 和線程 ID:
// thread_create.c
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
/* print pid and tid. */
void print_id(const char *s) {
printf("%s pid %lu, tid 0x%lx\n", s,
(unsigned long)getpid(),
(unsigned long)pthread_self());
}
/* thread fun */
void *thread_fun(void *arg) {
print_id("new thread: ");
return NULL;
}
int main(void) {
pthread_t tid;
// create pthread
if (pthread_create(&tid, NULL, thread_fun, NULL) != 0) {
printf("thread create failed.\n");
return -1;
}
print_id("main thread: ");
// 等待子線程執(zhí)行完,后面會用 pthread_join 代替
sleep(1);
return 0;
}
編譯需要鏈接 -lpthread 線程庫,運行結(jié)果如下:
orange@ubuntu:~/$ gcc -Wall thread_create.c -lpthread -o thread_create
orange@ubuntu:~/$ ./thread_create
new thread: pid 21301, tid 0x7f05c2ee3700
main thread: pid 21301, tid 0x7f05c36bf700
從結(jié)果可看出兩個線程的進程 pid 是相同的,因為 2 者都所屬同一個進程,但是線程 tid 就不同了。另外,大家以后在用 gcc 編譯的時候盡量都加上 -Wall 來開啟所有的警告,可以幫助我們編寫更加嚴謹?shù)拇a。
線程終止,等待
單個線程可以通過 3 種方式退出,并且不會終止整個進程:
- 線程直接返回
- 線程被其他線程取消
- 線程調(diào)用 pthread_exit
這里主要介紹第 3 種方法,先來看看這個函數(shù)的定義:
#include <pthread.h>
void pthread_exit(void *retval);
其中 retval 返回的值可以通過調(diào)用 pthread_join 函數(shù)訪問:
#include <pthread.h>
/*
* thread: 要等待的線程 ID
* retval: 線程的退出狀態(tài)
* return: 成功返回 0,失敗返回錯誤編號
*/
int pthread_join(pthread_t thread, void **retval);
調(diào)用線程將一直阻塞,直到線程 ID 為 thread 的線程調(diào)用 pthread_exit,被取消或從啟動例程中返回。并且,進程中的其他線程可以通過調(diào)用 pthread_join 函數(shù)獲得該線程的退出狀態(tài)。其中 retval 有 2 種情況:
- 如果不為 NULL,則拷貝線程的退出狀態(tài)碼到
retval - 如果目標線程被取消,
PTHREAD_CANCELED被放到retval中
下面結(jié)合上面這兩個函數(shù)來看一個例子:主線程開啟 2 個子線程,然后分別使用 return 返回和 pthread_exit 退出,最后在主線程中獲取線程的返回碼。
#include <stdio.h>
#include <pthread.h>
void *thread_fun1(void *arg) {
printf("thread 1 return.\n");
return ((void *)1);
}
void *thread_fun2(void *arg) {
printf("thread 2 exit.\n");
pthread_exit((void *)2);
}
int main(void) {
pthread_t tid1, tid2;
void *ret_val = NULL;
// create thread 1 and 2
pthread_create(&tid1, NULL, thread_fun1, NULL);
pthread_create(&tid2, NULL, thread_fun2, NULL);
// wait thread 1
pthread_join(tid1, &ret_val);
printf("thread 1 exit code %ld\n", (long)ret_val);
// wait thread 2
pthread_join(tid2, &ret_val);
printf("thread 2 exit code %ld\n", (long)ret_val);
return 0;
}
編譯,運行可以看到成功獲得了 2 個線程的退出碼:
gcc -Wall thread_join.c -o thread_join -lpthread
./thread_join
thread 1 return.
thread 1 exit code 1
thread 2 exit.
thread 2 exit code 2
線程分離
Posix 也給我們提供了分離線程的函數(shù),當分離一個線程后,該線程在終止時,線程資源由系統(tǒng)自動釋放,不需要其他線程再次 join 等待它。:
#include <pthread.h>
// thread: 要分離的線程 ID,成功返回 0,失敗返回錯誤碼
int pthread_detach(pthread_t thread);
注意,如果嘗試分離一個已經(jīng)分離的線程會產(chǎn)生未定義的行為。該函數(shù)的使用方法很簡單,只需要一行代碼:
#include <pthread.h>
// 在主線程創(chuàng)建 tid 線程
pthread_create(&tid, NULL, thread_fun, NULL);
// 從主線程分離 tid 線程
pthread_detach(tid);
線程同步
當多個線程同時訪問共享資源時會產(chǎn)生線程安全的問題,我們需要使用一些技術(shù)來使得這些線程同步訪問共享資源(一個一個訪問,不同時訪問),并且使它們訪問變量的存儲內(nèi)存時不會訪問到無效的值。
線程同步的方法主要有互斥鎖,信號量等,前面在介紹 IPC 的時候使用信號量進行了進程間的通信,其中的信號量操作也是適用與線程的,所以這次主要介紹的同步方法是:互斥鎖 Mutex。
使用互斥鎖進行線程同步的基本思想是:線程在獲取共享資源的訪問前首先需要獲得鎖,沒有獲取則阻塞或返回,訪問結(jié)束后必須釋放鎖,偽代碼如下:
mutex_lock();
operate share resource
mutex_unlock();
Posix 線程給我們提供下面這些函數(shù)來操作互斥鎖。
初始化和銷毀 Mutex
#include <pthread.h>
// 動態(tài)初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 靜態(tài)初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 銷毀
int pthread_mutex_destroy(pthread_mutex_t *mutex);
靜態(tài)初始化比較簡單,動態(tài)初始化需要和銷毀 Mutex 的函數(shù)成對使用。
加鎖,解鎖 Mutex
#include <pthread.h>
// 加鎖,如果不能獲取鎖會阻塞調(diào)用進程
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 嘗試加鎖,不能獲取鎖就返回,不會阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);
例子:使用 Mutex 來保護共享資源
雖然 Mutex 的函數(shù)比較多,但是使用起來是很簡單的,只需要 4 步:初始化,加鎖,解鎖,銷毀(靜態(tài)初始化不需要),來看一個簡單的程序:
#include <stdio.h>
#include <pthread.h>
// 靜態(tài)初始化不需要 main 中的 1,2 兩步
//pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mymutex;
void* thread_fun(void* arg) {
// lock
pthread_mutex_lock(&mymutex);
for(int i = 0; i < 5; i++)
printf("thread num = %d\n", (int)arg);
// unlock
pthread_mutex_unlock(&mymutex);
return NULL;
}
int main() {
// 1. 動態(tài)初始化
pthread_mutex_init(&mymutex, NULL);
pthread_t mythread[3];
void* retval = NULL;
for (int i = 0; i < 3; i++) {
pthread_create(&mythread[i], NULL, thread_fun, (void *)i);
pthread_join(mythread[i], &retval);
}
// 2. 銷毀,與動態(tài)初始化成對使用
pthread_mutex_destroy(&mymutex);
return 0;
}
編譯,運行:
gcc -Wall lock_thread.c -o lock_thread -lpthread
./lock_thread
thread num = 0
thread num = 0
thread num = 0
thread num = 0
thread num = 0
thread num = 1
thread num = 1
thread num = 1
thread num = 1
thread num = 1
thread num = 2
thread num = 2
thread num = 2
thread num = 2
thread num = 2
這是我的機器的運行結(jié)果,可以將 pthread_mutex_lock 和 pthread_mutex_unlock 注釋掉,即不加鎖查看最后的結(jié)果會不會亂序。
線程池
線程池概念也是經(jīng)常遇到,這里來了解一下它的基本原理,這里沒有給出實現(xiàn),有興趣可以 Google 相關(guān)的線程池技術(shù)。來看一個 Web 服務的例子,以來了解為何使用線程池會有優(yōu)勢。
假設現(xiàn)在有一個多線程的 Web 服務器,沒有使用線程池前,每接受一個客戶端的連接請求就創(chuàng)建一個獨立線程來處理,但是如果請求較多就會嚴重影響系統(tǒng)性能,主要有 2 個原因:
- 創(chuàng)建很多線程需要耗費資源
- 一個線程做完任務后就被銷毀,不能重復利用
基于這兩個缺點,提出了線程池的概念:它的主要思想是在進程開始的時候就創(chuàng)建一定數(shù)量的線程,放到一起(稱為池)等待分配工作:
- 當服務器收到請求即喚醒一個線程處理任務,在一個線程處理完后會重新回到池中等待下一次的任務,使得線程可以重復使用
- 如果池中沒有可用線程,則服務器會等待直到有可用線程,使得系統(tǒng)不會再創(chuàng)建新的線程
線程池中的線程數(shù)量可以手動規(guī)定,也可以在系統(tǒng)運行時根據(jù)當前負荷動態(tài)調(diào)整,具有動態(tài)調(diào)整功能的線程池比較高級。
結(jié)語
多線程技術(shù)的應用非常廣泛,這篇文章主要介紹了在 Linux 下的 Posix 的標準的線程庫的使用方法,相關(guān) API 的使用其實不難,關(guān)鍵是要理解多線程的概念及為和要使用它。另外多線程中比較重要的是如何處理線程安全(同步)的問題,常見的處理方法有互斥鎖,信號量等,同步的話題比較復雜有興趣可以深入學習。
最后,感謝你的閱讀,我們下次再見 :)