從 0 開始學習 Linux 系列之「25.Posix 線程」

多線程

版權(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í)行的,效率提高了很多,也更加安全了,如下圖:

單線程和多線程的區(qū)別

使用多線程技術(shù)有下面幾個優(yōu)點:

  • 簡化任務的代碼:在單獨的線程中執(zhí)行一個任務可以使用簡單的同步編程模式
  • 共享進程資源:多個線程可以訪問相同的進程地址空間
  • 提高進程的吞吐量:使用多線程可以并行運行多個任務
  • 優(yōu)化程序體驗:可以使用多線程分開處理程序的輸入輸出

總體來說使用多線程技術(shù)可以優(yōu)化程序,提升用戶的體驗。了解了基本概念后,接下來看看操作系統(tǒng)實現(xiàn)線程的模型。

多線程模型

現(xiàn)在的操作系統(tǒng)有 2 種不同的方法來提供線程支持:

  1. 用戶線程:受內(nèi)核支持,無須內(nèi)核管理
  2. 內(nèi)核線程:由內(nèi)核直接支持和管理

這兩種方法之間有一定的聯(lián)系,畢竟用戶線程要受內(nèi)核的支持,有 3 種常見的建立兩者關(guān)系的模型:

  1. 多對一模型:多個用戶線程映射到一個內(nèi)核線程,多個線程不能并行執(zhí)行
  2. 一對一模型:一個用戶線程映射到一個內(nèi)核線程,創(chuàng)建線程的開銷較大
  3. 多對多模型:多個用戶線程可以復用同樣數(shù)量或更小數(shù)量的內(nèi)核線程,沒有前面 2 者的缺點

3 種模型圖如下:

3 種模型圖

實現(xiàn)的模型在不同的操作系統(tǒng)上都差不多,但是不同操作系統(tǒng)上的線程庫的實現(xiàn)卻是不大相同的。

線程庫

操作系統(tǒng)為我們提供創(chuàng)建和管理線程的 API,線程操作也有 2 種實現(xiàn)方法:

  1. 內(nèi)核支持的用戶線程庫:此庫的代碼和數(shù)據(jù)都存在用戶空間中,API 不會進行系統(tǒng)調(diào)用
  2. 原始的內(nèi)核級別的庫:此庫的代碼和數(shù)據(jù)都存在內(nèi)核空間,API 會進行系統(tǒng)調(diào)用

有 3 種主要的線程庫:

  1. Posix Pthread:POSIX 標準的拓展,可以提供用戶級和內(nèi)核級的庫,但僅僅是線程行為規(guī)范,而不是實現(xiàn),Linux,Solaris 等 OS 都實現(xiàn)了這個規(guī)范
  2. Win 32:適用于 Windows OS 的內(nèi)核級線程庫
  3. 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_equalman 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 種方式退出,并且不會終止整個進程:

  1. 線程直接返回
  2. 線程被其他線程取消
  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 種情況:

  1. 如果不為 NULL,則拷貝線程的退出狀態(tài)碼到 retval
  2. 如果目標線程被取消,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_lockpthread_mutex_unlock 注釋掉,即不加鎖查看最后的結(jié)果會不會亂序。

線程池

線程池概念也是經(jīng)常遇到,這里來了解一下它的基本原理,這里沒有給出實現(xiàn),有興趣可以 Google 相關(guān)的線程池技術(shù)。來看一個 Web 服務的例子,以來了解為何使用線程池會有優(yōu)勢。

假設現(xiàn)在有一個多線程的 Web 服務器,沒有使用線程池前,每接受一個客戶端的連接請求就創(chuàng)建一個獨立線程來處理,但是如果請求較多就會嚴重影響系統(tǒng)性能,主要有 2 個原因:

  1. 創(chuàng)建很多線程需要耗費資源
  2. 一個線程做完任務后就被銷毀,不能重復利用

基于這兩個缺點,提出了線程池的概念:它的主要思想是在進程開始的時候就創(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)鍵是要理解多線程的概念及為和要使用它。另外多線程中比較重要的是如何處理線程安全(同步)的問題,常見的處理方法有互斥鎖,信號量等,同步的話題比較復雜有興趣可以深入學習。

最后,感謝你的閱讀,我們下次再見 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容