第九篇 多線程

在說多線程之前,我們來回憶下棧的功能和用途:一個棧中只有最下方的幀可被讀寫,相應的,也只有該幀對應的那個函數被激活,處於工作狀態。爲了實現多線程,則必須繞開棧的限制。爲此,在創建一個新線程時,需要爲這個線程建一個新的棧,每個棧對應一個線程。當某個棧指向到全部彈出時,對應線程完成任務,並結束。所以多線程的進程在內存中有多個棧,多個棧之間以一定的空白區域隔開,以備棧的增長。

1. 多線程的創建與結束

  • 頭文件以及原型:
    #include <pthread.h>
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); ---->創建一個線程
    int pthread_join(pthread_t thread, void **retval);------>等待一個線程的結束
  • 線程的結束有兩種途徑:
    (1)函數已經結束,調用它的線程也就結束了;
    (2)通過函數pthread_exit來實現。—>原型:void pthread_exit(void *retval);
  • pthread_join和pthread_exit的區別:
    (1)pthread_join一般是主線程來調用,用來等待子線程退出,因爲是等待,所以是阻塞的,一般主線程會依次添加所有它創建的子線程;
    (2)pthread_exit一般是子線程調用,用來結束當前線程;
    (3)子線程可以通過pthread_exit傳遞一個返回值,而主線程通過pthread_join獲得該返回值,從而判斷該子線程的退出時正常還是異常。
    eg:
    void *retval;
    iRet = pthread_join(tid, &retval);
    if(iRet){
    printf(“pthread_join error: iRet = %d\n”, iRet);
    return iRet;
    }
    printf(“retval = %ld\n”, (long)retval);
    注意:猶豫本程序是在64位機器上執行的,所以指針類型和long類型大小相等,都是8byte,並且將temp強制轉換成log類型,如果強制轉換成int類型,編譯時會提示“error: cast from ‘void *’ to ‘int’ loses precision”
  • 獲取線程id
    (1)在線程調用函數中使用pthread_self函數來獲得線程id;
    (2)在創建線程時生成的id。
  • 線程的屬性介紹
    (1)線程屬性結構如下:
    typedef struct
    {
    int detachstate; //線程的分離狀態
    int schedpolicy; //線程調度策略
    structsched_param schedparam; //線程的調度參數
    int inheritsched; //線程的繼承性
    int scope; //線程的作用域
    size_t guardsize; //線程棧末尾的警戒緩衝區大小
    void* stackaddr; //線程棧的位置
    size_t stacksize; //線程棧的大小
    }pthread_attr_t;
    (2)線程分離狀態:detachstate
    該屬性決定了線程運行任務後以什麼方式來結束自己。
    方式如下:
    I. PTHREAD_CREATE_DETACHED —— 分離線程
    置爲分離線程的線程。當不須要被不論什麼線程等待,線程運行完任務後,自己自己主動結束線程,並釋放資源。
    II. PTHREAD_CREATE_JOINABLE(缺省) —— 可匯合線程
    可匯合線程爲線程的默認狀態,這樣的情況下,原有的線程等待創建的線程結束。僅僅有當pthread_join()函數返回時。創建的線程纔算終止。才幹釋放自己佔用的系統資源
    III.設置和獲取線程分離屬性的函數:
    int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *state);
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int state);
    (3)線程的調度策略:schedpolicy
    I. SCHED_FIFO(先進先出策略)
    FIFO線程持續執行,直至有更高優先級的線程就緒,或者線程本身進入堵塞狀態。
    當FIFO線程堵塞時,系統將其移出就緒隊列,恢復後再增加到同優先級就緒隊列的末尾。當FIFO線程被高優先級線程搶佔時。它在就緒隊列中的位置不變。因此一旦高優先級線程終止或堵塞,被搶佔的FIFO線程會馬上繼續執行
    II.SCHED_RR(輪轉策略)
    每一個RR線程會獲得一個時間片,一旦RR線程的時間片耗盡,系統即將移到就緒隊列的末尾。
    III. SCHED_OTHER(缺省)
    靜態優先級爲0。不論什麼就緒的FIFO線程或RR線程,都會搶佔此類線程。
    IV. 設置或獲取線程調度策略的函數:
    int pthread_getschedpolicy(pthread_attr_t *attr, int policy);
    int pthread_setschedparam(pthread_attr_t *attr, struct sched_param *param);
    (4)調度參數 : sched_param schedparam
    I.函數接口:
    int pthread_attr_getschedparam(const pthread_attr_t *restrict attr, struct sched_param *restrict param);
    int pthread_attr_setschedparam(pthread_attr_t *restrict attr, const struct sched_param *restrict param);
    II.這兩個函數具有兩個參數。第1個參數是指向屬性對象的指針。第2個參數是sched_param結構或指向該結構的指針。結構sched_param在文件/usr/include/bits/sched.h中定義例如以下:
    struct sched_param
    {
    intsched_priority;
    };
    結構sched_param的子成員sched_priority控制一個優先權值,大的優先權值相應高的優先權。
    系統支持的最大和最小優先權值能夠用sched_get_priority_max函數和sched_get_priority_min函數分別得到。
    注意:假設不是編寫實時程序,不建議改動線程的優先級。由於,調度策略是一件很複雜的事情,假設不對使用會導致程序錯誤,從而導致死鎖等問題。
    如:在多線程應用程序中爲線程設置不同的優先級別,有可能由於共享資源而導致優先級倒置。

    (5)線程的繼承性: inheritsched
    I. PTHREAD_INHERIT_SCHED(缺省) —— 調度屬性自創建者線程繼承
    II. PTHREAD_EXPLICIT_SCHED —— 調度屬性由調度參數和調度策略決定
    繼承性決定調度的參數是從創建的進程中繼承還是使用在schedpolicy和schedparam屬性中顯式設置的調度信息。Pthreads不爲inheritsched指定默認值,因此假設你關心線程的調度策略和參數,必須先設置該屬性。
    III. 新線程不繼承父線程調度優先級;
    (6)線程的作用域:scope
    線程的競爭範圍。
    I. PTHREAD_SCOPE_SYSTEM ——在系統範圍內競爭資源。
    II. PTHREAD_SCOPE_PROCESS(Linux不支持)——在進程範圍內競爭資源
    III. 函數接口:
    int pthread_attr_getscope(const pthread_attr_t *restrict attr, int *restrict contentionscope);
    int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
    (7)線程棧末尾的警戒緩衝區大小:guardsize
    該屬指定線程末尾的警戒緩衝區大小,在缺省的情況下爲一個內存頁(4096字節)
    int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
    int pthread_attr_setguardsize(pthread_attr_t *attr, size guardsize);
    (8)線程棧的位置:stackaddr
    int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **addr);
    int pthread_attr_setstackaddr(pthread_attr_t *attr, void *addr);
    (9)線程棧的大小:stacksize
    int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *size);
    int pthread_attr_setstacksize(pthread_attr_t *attr, size_t size);
    int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *size);
    int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size size);

2 多線程的同步

多線程中在訪問共享內存時(比如全局變量)會存在競爭條件。而最常見的解決競爭條件的方法是將原先分離的兩個指令構成不可分割的一個原子操作,而其他任務不能插入到原子操作中。
對於多線程來說,同步是指在一定的時間內只允許某一個線程訪問某個資源。同步方法有互斥鎖(mutex)、條件變量(condition variable)、讀寫鎖(reader-writer lock)和信號量(semphore)

2.1 互斥鎖

  • 接口:
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr);
    int pthread_mutex_destory(pthread_mutex_t *mutex);
    int intpthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • pthread_mutex_trylock與pthread_mutex_lock不同的是在鎖以及被佔據時返回EBUSY,而不是掛起等待;
  • 鎖的創建方式有兩種
    靜態:pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZER
    動態:使用pthread_mutex_init函數

2.2 條件變量

條件變量時通過允許線程阻塞和等待另一個線程發送信號的方法彌補互斥鎖的不足,它常與互斥鎖一起使用。

  • 接口:
    (1)創建:
    靜態:pthread_cond_t cond = PTHREAD_COND_INITIALIZER
    動態:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
    (2)註銷:
    int pthread_cond_destroy(pthread_cond_t *cond);
    (3)等待:
    int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
    int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
    (4)激發:
    pthread_cond_signal()激活一個等待該條件的線程,存在多個等待線程時按入隊順序激活其中一個;pthread_cond_broadcast()則激活所有等待線程。

2.3 讀寫鎖

一種訪問必須是排他性的,就是獨佔的意思,這稱作寫操作;另一種情況就是訪問方式可以是共享的,就是說可以有多個線程同時去訪問某個資源,這種稱作讀操作。

  • 讀寫鎖比起互斥鎖具有更高的使用性與並行性,可以有多個線程同時佔用讀模式的讀寫鎖,但是只能有一個線程佔用寫模式的讀寫鎖,讀寫鎖的3種狀態如下描述:
    (1)當讀寫鎖是寫加鎖狀態時,在這個鎖被解鎖之前,所有試圖對這個鎖加鎖的線程都會被阻塞;
    (2)當讀寫鎖在讀加鎖狀態時,所有試圖以讀模式對它進行加鎖的線程都可以得到訪問權,但是以寫模式對它進行加鎖的線程將會被阻塞;
    (3)當讀寫鎖在讀模式的鎖狀態時,如果有另外的線程試圖以寫模式加鎖,讀寫鎖通常會阻塞隨後的讀模式鎖的請求,這樣可以避免讀模式鎖長期佔有,而等待的寫模式鎖請求則長期阻塞。
  • 函數接口
    (1)初始化和銷燬讀寫鎖
    初始化:
    I、靜態分配的讀寫鎖賦予常值:PTHREAD_RWLOCK_INITIALIZER
    II、動態初始化:int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
    銷燬:
    int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);
    (2)如果使用非默認屬性,則要使用到下面的兩個函數:
    int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
    int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
    (3)獲取和釋放讀寫鎖
    阻塞型:
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);
    非阻塞型:
    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
    int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);

2.4 信號量

信號量和互斥鎖的區別:互斥鎖只允許一個線程進入臨界區,而信號量允許多個線程同時進入臨界區。

  • 頭文件:semaphore.h
  • 創建信號量: int sem_wait(sem_t *sem, int pshared, unsigned int value);
    pshared控制信號量的類型,如果其值爲0,就表示這個信號量是當前進程的局部信號量,否則信號量就可以在多個進程之間共享;value爲sem的初始值;調用成功返回0,失敗返回-1;
  • sem_wait函數: int sem_wait(sem_t *sem);該函數用於以原子操作的方式將信號量的值減1.原子操作就是,如果兩個線程企圖同時給一個信號量加1或減1,它們之間不會互相干擾;
  • sem_post函數:int sem_post(sem_t *sem);該函數用於以原子操作的方式將信號量加1.
  • 對用完的信號量進行清理:int sem_destroy(sem_t *sem);

2.5 多線程重入

  • 可重入函數特點:
    (1)不爲連續的調用持有靜態數據;
    (2)不返回指向靜態數據的指針;
    (3)所有數據都由函數的調用者提供;
    (4)使用本地數據,或者通過製作全局數據的本地副本來保護全局數據;
    (5)如果必須訪問全局變量,要利用互斥鎖、信號量等來保護全局變量;
    (6)絕不調用任何不可重入函數。
  • 不可衝入函數特點:
    (1)函數中使用靜態變量,無論是全局靜態變量還是局部靜態變量;
    (2)函數返回靜態變量;
    (3)函數中調用了不可重入函數;
    (4)函數體內使用靜態的數據結構;
    (5)函數體內調用了malloc()或者free()函數;
    (6)函數體內調用其他標準I/O函數。
  • 編寫的多線程程序,通過定義_REENTRANT來告訴編譯器需要可重入功能,這個宏的定義必須出現於程序中的任何#include語句之前。
  • _REENTRAND爲我們做三件事,並且做的非常優雅:
    (1)它爲對部分函數重新定義它們的可安全重入的版本,這些函數名字一般不會發生改變,只是會在函數名後面添加_r字符串,如函數名gethostbyname變成gethostbyname_r;
    (2)stdio.h中原來以宏的形式實現的一些函數將變成可安全重入函數;
    (3)在error.h中定義的變量error現在將變成一個函數調用,它能夠以一種安全的多線程方式來獲取真正的errno的值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章