Linux環境:C編程之多線程

線程的創建與退出

線程創建函數

函數原型
int pthread_create(pthread_t* thread, pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);

返回值與參數

  • 返回值:成功,則返回 0;失敗,則返回對應錯誤碼。
  • 參數 thread 是傳出參數,保存新線程的標識;
  • 參數 attr 是一個結構體指針,結構中的元素分別指定新線程的運行屬性,attr 可以用pthread_attr_init 等函數設置各成員的值,但通常傳入爲 NULL 即可
  • 參數 start_routine 是一個函數指針,指向新線程的入口點函數,線程入口點函數帶有一個 void *的參數由 pthread_create 的第 4 個參數傳入;
  • 參數 arg 用於傳遞給第 3 個參數指向的入口點函數的參數,可以爲 NULL,表示不傳遞。
線程退出
void pthread_exit(void *retval);

函數 pthread_exit 用在線程函數中表示線程的退出。其參數可以被其它線程用 pthread_join 函數捕獲。

int pthread_join(pthread_t th, void **thread_return);

用於回收線程返回值,相當於進程中的waitwaitpid
th指定要回收的線程,thread_return 是一個傳出參數,接收線程函數的返回值。

示例程序

創建一個子線程,傳入數值1,在子線程中能夠獲取並打印,子線程退出,返回數值2,主線程通過pthread_join獲取等待子線程結束並獲取子線程的退出值並打印

#include <fun.h>

void* thread1(void * val)
{
    printf("線程創建成功,傳入參數:%d\n",(int)val);
    pthread_exit((void *)2);
}
int main(int args,char *argv[])
{
    pthread_t p=0;
    int ret =  pthread_create(&p,NULL,thread1,(void *)1);
    THREAD_CHECK(ret,"pthread_create")
    pthread_join( p,(void **)&ret);

    printf("父進程回收:%d\n",ret);
    
    return 0;
}

執行效果:
在這裏插入圖片描述

線程的終止與清理

線程終止
  • int pthread_cancel(pthread_t thread);//向thread線程發送cancel信號
  • 通過其他線程向目標線程發送cancel信號可以使目標線程按照設計好的方式終止。
  • 目標線程收到信號後或者忽略、或者立即終止、或者繼續運行至 cancelation-point(取消點)後結束。
  • 取消點一般是能引起阻塞的系統調用,在設置取消點時應在系統調用前後加上pthread_testcancel()調用來符合到 POSIX 標準。
線程清理

線程運行過程中常常會意外終止,無法正常釋放掉自己所佔用的資源從而對其他線程造成影響,或者造成系統資源浪費,所以需要引入線程清理機制。

POSIX 線程 API 提供了以下函數對用於在進程結束時自動釋放資源:

void pthread_cleanup_push(void (*routine) (void *), void *arg)/void pthread_cleanup_pop(int execute)

  • 用法:在線程中成對出現,對兩個函數段之間的代碼段進行保護,如果線程運行在清理函數中間的代碼段時終止,則執行定義好的清理函數。如果有多個清理函數對,則遵循先入後出的規則,類似括號匹配規則
  • pthread_cleanup_push函數的參數爲一個函數指針指向清理函數,和傳給該清理函數的參數
  • pthread_cleanup_pop函數從參數爲0表示執行到該函數時不執行清理函數,參數非0則執行清理函數。該參數不影響線程在pthread_cleanup_pop之前退出時的自動清理。
示例程序

創建一個子線程,子線程申請內存,通過清理函數進行free,子線程停留在read標準輸入,主線程cancel子線程,子線程能夠通過清理函數free對應的malloc的內存

#include <fun.h>
void freept(void * p)
{
    printf("開始清理!\n");
    free((char *)p);
}
void* thread1(void * val)
{

    printf("線程創建成功!\n");
    char *buf=(char *)malloc(128);
    pthread_cleanup_push(freept,(void *)buf);
    pthread_testcancel();
    read(0,buf,128);
    pthread_testcancel();
    pthread_cleanup_pop(0);
    pthread_exit((void *)0);
}
int main(int args,char *argv[])
{
    pthread_t p=0;
    int ret =  pthread_create(&p,NULL,thread1,NULL);
    THREAD_CHECK(ret,"pthread_create")
    sleep(1);
    printf("主進程開始發送cancel信號\n");
    sleep(1);
    ret =  pthread_cancel(p);
    THREAD_CHECK(ret,"pthread_cancel")
    long ptret;
    pthread_join(p,(void**)&ptret);
    printf("子線程退出碼:%ld\n", ptret);
    return 0;

}

執行效果:
在這裏插入圖片描述

線程互斥與同步

線程互斥鎖pthread_mutex_t

線程互斥通過pthread_mutex_t實現,相當於一個互斥信號量,線程在訪問臨界區時進行加鎖競爭鎖資源,結束訪問後解鎖,從而避免了線程之間讀寫公共的進程空間數據發生衝突。

互斥鎖的創建和銷燬

對於互斥鎖類型變量,有兩種初始化方式:

  • 靜態創建
    直接宏定義賦值,只能創建普通鎖
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 動態創建
    使用初始化函數,可以指定互斥鎖的屬性:
    int pthread_mutex_init(互斥鎖指針, 互斥鎖屬性指針)
  • 銷燬
    只有互斥鎖沒有上鎖時纔可以被銷燬,否則返回錯誤
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥鎖屬性

互斥鎖在初始化時可以指定屬性,互斥鎖的屬性用pthread_mutexattr_t描述,該結構體的定義如下:

typedef struct
{
int __mutexkind; //注意這裏是兩個下劃線
} pthread_mutexattr_t;

該變量有三種宏定義的值可以選:

  • PTHREAD_MUTEX_TIMED_NP:即爲0,NULL,缺省值,普通鎖,當一個線程加鎖以後,其餘請求鎖的線程將形成一個阻塞等待隊列,並在解鎖後按優先級獲得鎖。這種鎖策略保證了資源分配的公平性。
  • PTHREAD_MUTEX_RECURSIVE_NP:嵌套鎖,一個進程可以多次加鎖,不同線程請求,則在加鎖線程解鎖時重新競爭,而非先來先得
  • PTHREAD_MUTEX_ERRORCHECK_NP:檢錯鎖,不允許一個進程對同一個互斥鎖多次加鎖,否則報錯。可以避免最簡單的二次加鎖造成的死鎖
鎖操作函數
  • 加鎖 int pthread_mutex_lock(pthread_mutex_t *mutex)

    加鎖,不論哪種類型的鎖,都不可能被兩個不同的線程同時得到,而必須等
    待解鎖。對於普通鎖類型,解鎖者可以是同進程內任何線程;而檢錯鎖則必須由加鎖者解鎖纔有效,否則返回 EPERM;對於嵌套鎖,文檔和實現要求必須由加鎖者解鎖,但實驗結果表明並沒有這種限制,這個不同目前還沒有得到解釋。在同一進程中的線程,如果加鎖後沒有解鎖,則任何其他線程都無法再獲得鎖。

  • 解鎖 int pthread_mutex_unlock(pthread_mutex_t *mutex)

    • 對於快速鎖,pthread_mutex_unlock 解除鎖定;
    • 對於遞規鎖,pthread_mutex_unlock 使鎖上的引用計數減 1;
    • 對於檢錯鎖,如果鎖是當前線程鎖定的,則解除鎖定,否則什麼也不做。
  • 測試加鎖 int pthread_mutex_trylock(pthread_mutex_t *mutex)

    與加鎖函數行爲一致,但是如果鎖已經被佔據,返回 EBUSY,而不是掛起等待

線程同步

通過全局的條件變量可以實現進程同步。
條件變量工作的原理是在線程需要協調工作的地方對條件變量進行操作使得進程掛起,等待信號。在另一線程滿足條件的地方激活條件變量使得信號可以繼續運行。
可以看到條件變量是臨界資源,所以使用時需要搭配互斥鎖。

  • 條件變量初始化
    • 靜態初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    • 動態初始化:int pthread_cond_init(pthread_cond_t *cond, NULL)
  • 條件變量的等待
    • 無條件等待:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    • 計時等待:int pthread_cond_timedwait(條件變量, 互斥鎖, const struct timespec *abstime(等待時間));
  • 條件變量的激活
    • pthread_cond_signal(條件變量)激活一個等待該條件的線程
    • pthread_cond_broadcast()則激活所有等待線程
示例程序

創建兩個子線程賣票,第三個子線程放票,兩個子線程賣完20張票後,第三個子線程把票重新設置爲20,然後兩個子線程繼續賣票,把票賣光

#include <fun.h>
typedef struct
{
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int ticket;
    int flag;
}data;


data test={PTHREAD_MUTEX_INITIALIZER,PTHREAD_COND_INITIALIZER,0,1};
void *give(void *p)
{

    pthread_mutex_lock(&test.mutex);
    test.ticket = 20;
    printf("放票員:第一次放票完成\n");
    pthread_mutex_unlock(&test.mutex);
    pthread_mutex_lock(&test.mutex);
    pthread_cond_wait(&test.cond,&test.mutex);
    test.ticket = 20;
    test.flag = 0;
    printf("放票員:第二次放票完成\n");
    pthread_mutex_unlock(&test.mutex);
    printf("放票員:我下班了!\n");
}

void *sale1(void *p)
{
    int sum =0;
    while(1)
    {
        pthread_mutex_lock(&test.mutex);
        if(test.ticket>0)
        {
            test.ticket--;
            sum++;
            if(test.ticket==10)
            {
                printf("售票員1:餘票還有10張!\n");
            }
            if(test.ticket==0)
            {
                pthread_cond_signal(&test.cond);
            }
                pthread_mutex_unlock(&test.mutex);
        }
        else
        {
            if(test.flag) 
            {   
                pthread_mutex_unlock(&test.mutex);
                sleep(1);
                continue;
            }
            else
            {
                pthread_mutex_unlock(&test.mutex);
                break;
            }
        }
    }
    printf("售票完成,售票員1賣了%d張票\n",sum);
}

void *sale2(void *p)
{
    int sum =0;
    while(1)
    {
        pthread_mutex_lock(&test.mutex);
        if(test.ticket>0)
        {
            test.ticket--;
            sum++;
            if(test.ticket==10)
            {
                printf("售票員2:餘票還有10張!\n");
            }
            if(test.ticket==0)
            {
                pthread_cond_signal(&test.cond);
            }
                pthread_mutex_unlock(&test.mutex);
        }
        else
        {
            if(test.flag)
            {   
                pthread_mutex_unlock(&test.mutex);
                sleep(1);
                continue;
            }
            else
            {
                pthread_mutex_unlock(&test.mutex);
                break;
            }
        }
    }
    printf("售票完成:售票員2賣了%d張票\n",sum);
}
int main(int args,char *argv[])
{

    pthread_t p1,p2,c0;
    pthread_create(&p1,NULL,sale1,NULL);
    pthread_create(&p2,NULL,sale2,NULL);
    pthread_create(&c0,NULL,give,NULL);
    pthread_join(c0,NULL);
    pthread_join(p1,NULL);
    pthread_join(p2,NULL);
    return 0;
}

線程安全性

什麼是線程安全性

當存在共享資源的時候,對資源的訪問需要同步。這時候使用線程編寫程序的時候,需要編寫具有線程安全性屬性的函數。一個函數,當且僅當被多個併發線程反覆調用時,能夠一直產生正確的結果,才能夠被稱爲線程安全的(thread-safe),否則我們稱其爲非線程安全的。

不安全進程的四種情況
  • 讀寫共享變量
    若干線程併發訪問同一變量或共享內存,容易發生讀寫衝突,不安全
    解決方法:使用線程互斥鎖
  • 函數調用依賴於上次調用的結果
    當多個線程調用了同一函數,而該函數的執行結果又依賴於上次調用的結果時,會導致線程不安全,應注意避免使用帶有局部靜態變量的函數,或者重寫相關函數。
  • 調用返回指向靜態變量的指針的函數
    若干線程調用同一函數,該函數返回的是指向一個靜態變量的指針,則後調用的線程將會覆蓋先調用函數的返回結果,比如ctime函數。應該重寫函數,或者通過加鎖-複製-解鎖的方式把指針返回值及時保存下來。
  • 調用線程不安全函數的函數
    線程調用的函數本身存在上述三種情況,如果是第二中則必須重寫該函數,如果是1、3種情況可以通過加鎖保護來使線程安全。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章