【操作系統】在 Linux 上編寫和調試多線程程序

本文的環境:
Linux centos-7.shared 3.10.0-693.5.2.el7.x86_64 #1 SMP Fri Oct 20 20:32:50 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)

本文使用 POSIX 線程庫,需引入頭文件 pthread.h,在編譯的時候要注意添加 -lpthread 參數。

POSIX 是一個標準,約定一個操作系統應該提供哪些接口,pthread 即爲 posix thread。C++11、Python、Java 均內置了線程庫。

線程創建

int pthread_create(
	pthread_t *thread, 
	const pthread_attr_t *attr,
	void *(*start_routine) (void *), 
	void *arg
);

參數:
thread 是一個輸出型參數,返回一個線程 id。
attr 可以設置線程屬性,填 NULL 表示使用默認屬性。
start_routine 是一個函數指針,是線程執行的入口函數。
arg 是 start_routine 的函數參數。
值得注意的是這個函數 arg 是不支持傳遞多個參數的(可變參數),如果需要傳遞多個函數就需要使用 struct 或者一些別的方式。

返回值:
成功返回 0,失敗返回錯誤碼。

栗子:
創建一個線程,傳遞一個參數,並在這個新線程內打印這個參數。
在這裏插入圖片描述

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

struct ThreadArg {
    int num;
};

void* ThreadEntry(void* arg) {
    while (1) {
        printf("In ThreadEntry, %lu, arg %d\n", pthread_self(), ((struct ThreadArg*) arg)->num);
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    struct ThreadArg ta;
    ta.num = 20;
    pthread_create(&tid, NULL, ThreadEntry, &ta);
    while (1) {
        printf("In Main Thread, %lu\n", pthread_self());
        sleep(1);
    }

    return 0;
}

注意:
上面代碼中使用了 pthread_self() 獲取線程的 id,這是 POSIX 線程庫提供的庫函數,操作系統也提供了一個獲取線程 id 的系統調用 gettid()

pthread_t pthread_self(void);
pid_t gettid(void);

但是當你在同一個線程調用兩個函數的時候發現返回的並不是相同的值。

The thread ID returned by this call is not the same thing as a POSIX thread ID.
(i.e., the opaque value returned by pthread_self(3)).
對於單線程的進程,內核中tid==pid,對於多線程進程,他們有相同的pid,不同的tid。tid用於描述內核真實的pid和tid信息。
pthread_self返回的是posix定義的線程ID,man手冊明確說明了和內核線程tid不同。它只是用來區分某個進程中不同的線程,當一個線程退出後,新創建的線程可以複用原來的id。

gettid() 和 pthread_self()的區別

線程終止

想讓一個線程結束而不終止進程:
1、從線程處理函數 return
2、線程自己調用 pthread_exit 切腹自盡。

void pthread_exit(void *retval);

參數:輸入 & 輸出型參數。
注意:pthread_exit 參數所指向的內存單元必須是全局的或者是用malloc 分配的,不能在線程函數的棧上分配,因爲當其它線程得到這個返回指針時線程函數已經退出了。

3、兄弟線程調用 pthread_cancel 終止同一進程中的另一個線程。

int pthread_cancel(pthread_t thread);

參數:線程 id
返回值:成功返回 0,失敗返回錯誤碼。
注意:這個 pthread_cancel 是溫和的終止一個線程而不是強制 kill 掉,抽象一個例子就是你在召喚師峽谷殺敵ing,你媽喊你喫飯,你可能得等一會纔過去喫飯。

線程等待

爲什麼要線程等待?
例如,計算一個很大的矩陣相乘,可以使用多線程方式來計算,每個線程計算其中的一部分,最終等待所有的線程執行完,主線程彙總結果,這裏就用 pthread_join 來保證邏輯。

int pthread_join(pthread_t thread, void **retval);

參數:thread 線程 id,retval 指向一個指針,這個指針指向線程的返回值,不關注線程的返回值可以填 NULL。
返回值:成功返回 0,失敗返回錯誤碼。
調用該函數的線程將掛起等待,直到 id 爲 thread 的線程終止。thread 線程以不同的方法終止,通過 pthread_join 得到 的終止狀態是不同的:
1、如果thread線程通過 return 返回, value_ ptr 所指向的單元裏存放的是thread線程函數的返回值。
2、如果thread線程被別的線程調用 pthread_cancel 異常終掉, value_ ptr 所指向的單元裏存放的是常數 PTHREAD_CANCELED。
3、如果thread線程是自己調用 pthread_exit 終止的,value_ptr 所指向的單元存放的是傳給 pthread_exit 的參數。

線程分離

類似於忽略 SIGCHLD 信號,分離後就代表線程的死活不管了,也就不用 pthread_join 回收了。

int pthread_detach(pthread_t thread);

可以自己把自己分離出去,也可以被兄弟線程分離。
pthread_detach(pthread_self());

使用 gdb 調試多線程程序

0、使用 gdb 調試多線程程序

使用 gdb 調試一個程序需要在編譯時候加上 -g 選項,爲什麼?百度去。

gdb attach 28966
info thread # 查看所有線程信息
bt # 查看當前線程調用棧
thread 2 # 切換當前線程

在這裏插入圖片描述

1、查看一個程序的所有線程信息

ps -eLf | grep a.out

參數:-L 表示 LWP,這裏 ps 得到的線程 id 是和 gettid 一樣的。
在這裏插入圖片描述

2、查看一個程序依賴哪些庫

ldd a.out

在這裏插入圖片描述
3、查看一個進程中有幾個線程和線程調用棧

pstack 27779

在這裏插入圖片描述


EOF

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章