POSIX多線程初步

寫在前面
本文內容 本文是我學習 POSIX 多線程編程的一些學習筆記和心得。
所需知識 如果有網友對這篇筆記有興趣,您需要具備一些操作系統的知識,尤其是處理機調度、進程與線程方面。
閱讀時間 大約需要10分鐘


進程與線程基本概念簡介

進程是程序運行時的一個實例,同一個程序可以有多個實例即多個進程。進程是操作系統進行資源分配(CPU時間片、內存空間等等)的基本單位,進程擁有各自獨立的邏輯地址空間。

線程是進程的一個執行流,是操作系統進行處理機調度的基本單位。

凡創建一個進程,其自身就隱含一個“主”線程,額外創建線程時,操作系統調用 clone() 操作克隆出一個與當前進程完全相同的環境,各線程間相互之間共享邏輯地址空間和各類數據、資源,但線程擁有自己獨立的棧和局部變量。

這些線程都運行在進程的地址空間中,這意味著如果進程崩潰,該進程下的所有線程都會終止,同樣的,線程崩潰同樣會造成進程終止。

可以看出,多進程更加安全,一個進程崩潰不會影響其他進程,多線程相對不是那么健壯。但進程間切換時資源耗費比較大,效率相較于多線程會低一些。所以對于一些高并發、高共享的操作,多線程更優。

Pthread 即指 POSIX 多線程,是 UNIX 世界給出的一套操作多線程的系統接口,都在頭文件pthread.h中聲明。


Pthreads API

Pthreads API 中大致有100個函數調用,可分為一下四類:

  • 線程管理,如:線程創建、線程終止、線程回收等
  • 互斥對象(mutex):包括鎖的創建、初始化、鎖定、解鎖、摧毀等
  • 條件變量:創建、摧毀、等待、通知、設置與查詢屬性等操作
  • 使用了讀寫鎖的線程間的同步管理

線程管理函數

函數概覽(省略參數):
pthread_create() 創建一個新的線程
pthread_exit() 線程自身退出
pthread_join() 阻塞當前線程,知道被等待的線程運行結束并返回
重要的數據類型:
pthread_t 定義 線程句柄,相當于線程ID

注:線程句柄,可以理解為“線程id”,其類型 pthread_t 定義在 pthread.h 中。

線程的創建與終止

線程使用pthread_create()函數創建,其函數原型是:

int pthread_create(pthread_t* tidp, constpthread_attr_t* attr, (void*)(*start_rtn)(void*), void* arg);

系統對最大線程數有限制,所以 pthread_create()可能不成功。若線程創建成功,則返回0。若線程創建失敗,則返回出錯編號。第一個參數是線程句柄,用于唯一標識一個線程,必須使用已經聲明為pthead_t型的變量;第二個參數是線程屬性,線程是可以擁有屬性的,如果沒有屬性則使用NULL;第三個參數是線程執行函數,一個線程在執行時必須執行一個函數,就像一個進程必須有一個main()函數一樣。這里將需要執行的函數的函數名傳遞進去就可以了。第四個參數是傳給線程的參數,如果線程執行函數是有參數的,則在這里進行傳遞。

這里需要特別注意兩點:

一、線程函數

線程函數包含了線程執行時的所有代碼,在定義時應當像下面這樣定義:

void *func_thread(void * args) {
    // 這里對 (void *) args 進行強制類型轉換
    /* code */
    // 有返回值使用:pthread_exit((void *) returns);
    // 無返回值使用:pthread_exit(NULL);
}
  1. 函數無論有沒有參數,都必須接受一個無類型指針。如果確實需要參數,則在函數內部進行參數強制類型轉換,將無類型轉換成需要的類型。

  2. 函數執行完畢必須使用pthread_exit()進行返回。它的作用是,終止調用它的線程并返回一個指向某個對象的指針。如果有返回值,則使用 pthread_exit((void *) returns) 在返回時將返回值轉換成無類型指針型再返回;如果沒有返回值,則使用pthread_exit(NULL) 返回空指針。

二、線程函數傳參

傳給線程的參數必須先轉換成void *類型。任何類型的對象都可以賦值給void *,但將void *轉換成其他類型則需要進行強制類型轉換。其隱含的意思是無類型可以包容有類型,而有類型不能直接包容無類型,因為可能出現不安全的情況,所以需要程序員自己掌控。pthread_create()函數無法得知線程函數需要什么類型的參數,所以使用無類型指針,表示可以傳進任何類型的參數。void *指針指向參數的地址,之后在線程函數內部對參數類型進行強制類型轉換。

線程合并

進程有父進程,子進程的層次關系,而線程無論是創建者還是被創建者都不具有層次關系,線程之間的地位是平等的。造成的結果之一就是對于進程而言,子進程必須由父進程合并,而線程可以由其他任意線程合并。

線程合并使用pthread_join(tid) 函數。其函數原型為:

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

該函數的作用是等待由pthread_t thread所指定的線程終止后,回收其所有資源,如果線程有返回值,則存儲在void **retval中。

線程操作綜合示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
 
void printids(const char *s)
{
    pid_t pid;         //聲明進程句柄
    pthread_t tid;     //聲明線程句柄
    pid = getpid();    //該函數獲得當前進程句柄
    tid = pthread_self();    //該函數獲得當前線程句柄
    printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int) pid,
            (unsigned int) tid, (unsigned int) tid);
}
 
void *thr_fn(void *arg)    //線程函數,無類型參數,無類型返回值
{
    printids("new thread: ");
    pthread_exit(NULL);    //線程終止,無返回值
}
 
int main(void)
{
    int err;
    pthread_t ntid;    // 聲明線程句柄
    err = pthread_create(&ntid, NULL, thr_fn, NULL);    // 四個參數:線程句柄,線程屬性(無),線程函數,線程參數(無)
    if (err)   //如果pthread_create返回值不為0,即創建線程失敗
        printf("can't create thread: %s\n", strerror(err));
    printids("main thread:");
    pthread_join(ntid,NULL);    // 回收 ntid 線程
    return 0;
}

POSIX 多線程編譯時需要使用-lpthread參數,用以鏈接 POSIX 多線程庫,作者在自己電腦上編譯運行結果如下:

$ gcc -lpthread pthread_test.c 
$ ./a.out 
main thread: pid 9331 tid 4218988288 (0xfb78a700)
new thread:  pid 9331 tid 4210870016 (0xfafcc700)

互斥對象

同一進程下的所有線程都是共享進程的地址空間和其他共享資源的,當多個線程共同操作一個數據時,就可能會存在數據一致性問題。別擔心,POSIX 多線程庫為我們提供了互斥對象用于線程之間互斥地訪問互斥資源。

pthread_mutex_t mymutex 用于聲明一個互斥對象,互斥對象用于提供給程序一種互斥地訪問共享數據的機制,保證數據一致性。當一個線程要訪問一個共享資源時,必須先使用pthread_mutex_lock()對其上鎖,訪問結束后,必須使用pthread_mutex_unlock()釋放鎖。被上鎖的數據其他線程將無法訪問,如果發現資源已經上鎖則線程會被阻塞。

  • 上鎖函數原型:int pthread_mutex_lock(pthread_mutex_t * mutex)
  • 解鎖函數原型:int pthread_mutex_unlock(pthread_mutex_t * mutex)
  • 兩個函數成功返回0,失敗返回錯誤代碼

鎖定某個互斥對象時,也可以使用pthread_mutex_trylock(pthread_mutexattr_t * mutex)。上鎖之前該函數先檢測互斥對象,如果沒有被鎖定,則對其上鎖,如果已經被鎖定,則返回一個非0EBUSY錯誤值,但線程并不會阻塞,可以做其他事。

互斥對象聲明后要對其進行初始化,分為靜態初始化和動態初始化:

  • 靜態初始化:在對互斥對象進行聲明時直接賦值:
pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER

PTHREAD_MUTEX_INITIALIZER是在pthread.h中定義的一個常量。

  • 動態初始化:當代碼使用malloc()來分配一個新的互斥對象時,靜態初始化方法將不可用,需要使用pthread_mutex_init()來動態初始化。該函數的原型是:
int pthread_mutex_init(pthread_mutex_t * mymutex, const pthread_mutexattr_t * attr)

初始化成功返回0,第二個參數是用來設置互斥對象的屬性的。這里需要說明,互斥對象是有屬性的,但一般情況下并不需要,所以很多時候*attr都為NULL。

使用pnthread_mutex_init()來初始化一個互斥對象,則最后必須用pthread_mutex_destory()來銷毀它,釋放互斥對象所占用的資源。這類似于 C++ 里面,我們用new創建一個對象后,最后需要用delete來銷毀它。其原型為:

int pthread_mutex_destory(pthread_mutex_t * mymutex)

銷毀mymutex所指向的互斥對象,成功返回0。


互斥對象數量的設置要合理:如果過多,將會導致并發性變差,甚至運行速度比串行還低;如果過少,則有可能保證數據一致性。

要達到互斥對象數量的合理,則應遵循原則為:

  1. 互斥對象用來互斥訪問“共享數據”,不要對非共享數據使用互斥對象。
  2. 如果程序邏輯上能夠確保任何時候都只有一個線程能存取特定數據結構,那么也不要使用互斥對象。
  3. 訪問共享數據時,無論是讀或寫,都應使用互斥對象。
  4. 學會從線程的角度審視代碼,并確保程序中每一個線程對內存的觀點都是一致和合適的。

版權聲明 自由轉載 - 保持署名 - 不可商用 - 不可演繹 (CC3.0 創意共享3.0許可證

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容