1. 程式人生 > >你真的懂執行緒同步麼?

你真的懂執行緒同步麼?

  前言:學程序時,學習的重點應該程序間通訊,而學習執行緒時,重點就應該是執行緒同步了。想過為什麼?fork建立子程序之後,子程序有自己的獨立地址空間和PCB,想和父程序或其它程序通訊,就需要各種通訊方式,例如無名管道(管道,我習慣這麼叫無名管道)、有名管道(命名管道)、訊號、訊息佇列、訊號量、共享記憶體等;而pthread_create建立子執行緒之後,子執行緒沒有獨立的地址空間,大部分資料都是共享的,如果同時訪問資料,就是造成混亂,所以要控制,就是執行緒同步了。

  一、同步概念

  為什麼要特意說一下同步概念呢?因為它跟其他領域的“同步”有些差異。

  所謂同步,即同時起步,協調一致。不同的物件,對“同步”的理解方式略有不同。如,裝置同步,是指在兩個裝置之間規定一個共同的時間參考;資料庫同步,是指讓兩個或多個數據庫內容保持一致,或者按需要部分保持一致;檔案同步,是指讓兩個或多個資料夾裡的檔案保持一致。等等

       而,程式設計中、通訊中所說的同步與生活中大家印象中的同步概念略有差異。“同”字應是指協同、協助、互相配合。主旨在協同步調,按預定的先後次序執行

  二、執行緒同步方式

  這篇部落格主要介紹四種方式,如下:

方式 通用標識
互斥鎖(互斥量) pthread_mutex_
讀寫鎖  pthread_rwlock_
條件變數 pthread_cond_
訊號量 sem_

    表中的“通用標識”,指的是那種同步方式的函式、型別都那麼開頭的,方便記憶;還有其他方式,自旋鎖、遮蔽,感覺不常用,有興趣可以閱讀APUE。

   三、互斥鎖(互斥量)

  1、介紹

  先來畫個圖,來簡單說明一下:  PS:依舊是全部落格園最醜圖,不接受反駁!

  

  Linux中提供一把互斥鎖mutex(也稱之為互斥量)。

  每個執行緒在對資源操作前都嘗試先加鎖,成功加鎖才能操作,操作結束解鎖。

       資源還是共享的,執行緒間也還是競爭的,                                                                

       但通過“鎖”就將資源的訪問變成互斥操作,而後與時間有關的錯誤也不會再產生了。

  2、主要函式  

  pthread_mutex_init函式    //初始化mutex,預設為1

       pthread_mutex_destroy函式  //銷燬鎖

       pthread_mutex_lock函式     //加鎖,加鎖不成功,一直阻塞在那等待

       pthread_mutex_trylock函式   //嘗試加鎖,加鎖不成功,直接返回

       pthread_mutex_unlock函式  //解鎖

  以上5個函式的返回值都是:成功返回0, 失敗返回錯誤號。  

  pthread_mutex_t 型別,其本質是一個結構體。為簡化理解,應用時可忽略其實現細節,簡單當成整數看待。

  變數mutex只有兩種取值1、0。

  • pthread_mutex_init函式

  初始化一個互斥鎖(互斥量) ---> 初值可看作1

        原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

        參1:傳出引數,呼叫時應傳 &mutex      

        這個restrict關鍵字可能第一次遇到,說明一下:只用於限制指標,告訴編譯器,所有修改該指標指向記憶體中內容的操作,只能通過本指標完成。不能通過除本指標以外的其他變數或指標修改

         參2:互斥量屬性。是一個傳入引數,通常傳NULL,選用預設屬性(執行緒間共享)。互斥鎖也可以用於程序間同步,需要修改屬性為程序間共享。 參APUE.12.4同步屬性

  1. 靜態初始化:如果互斥鎖 mutex 是靜態分配的(定義在全域性,或加了static關鍵字修飾),可以直接使用巨集進行初始化。e.g.  pthead_mutex_t muetx = PTHREAD_MUTEX_INITIALIZER;
  2. 動態初始化:區域性變數應採用動態初始化。e.g.  pthread_mutex_init(&mutex, NULL)

  其他函式就不解釋了,相對比較簡單。

  示例程式,主要對標準輸出進行加鎖,使主執行緒列印大寫“HELLO WORLD”,子執行緒列印小寫“hello world”,程式如下:

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

pthread_mutex_t mutex;

void err_thread(int ret, char *str)
{
    if (ret != 0) {
        fprintf(stderr, "%s:%s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}

void *tfn(void *arg)
{
    srand(time(NULL));

    while (1) {

        pthread_mutex_lock(&mutex);
        printf("hello ");
        sleep(rand() % 3);    /*模擬長時間操作共享資源,導致cpu易主,產生與時間有關的錯誤*/
        printf("world\n");
        pthread_mutex_unlock(&mutex);

        sleep(rand() % 3);

    }

    return NULL;
}

int main(void)
{
    int flag = 5;
    pthread_t tid;
    srand(time(NULL));

    pthread_mutex_init(&mutex, NULL);
    pthread_create(&tid, NULL, tfn, NULL);
    while (flag--) {

        pthread_mutex_lock(&mutex);

        printf("HELLO ");
        sleep(rand() % 3);
        printf("WORLD\n");
        pthread_mutex_unlock(&mutex);

        sleep(rand() % 3);

    }
    pthread_cancel(tid);                //  將子執行緒殺死,子執行緒中自帶取消點
    pthread_join(tid, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;                           //main中的return可以將整個程序退出
}
View Code

  編譯時也要記得鏈上-pthread。

  四、讀寫鎖

  1、特性

  (1)讀寫鎖是“寫模式加鎖”時, 解鎖前,所有對該鎖加鎖的執行緒都會被阻塞。

  (2)讀寫鎖是“讀模式加鎖”時, 如果執行緒以讀模式對其加鎖會成功;如果執行緒以寫模式加鎖會阻塞。

  (3)讀寫鎖是“讀模式加鎖”時, 既有試圖以寫模式加鎖的執行緒,也有試圖以讀模式加鎖的執行緒。那麼讀寫鎖會阻塞隨後的讀模式鎖請求。優先滿足寫模式鎖。讀鎖、寫鎖並行阻塞,寫鎖優先順序高

         讀寫鎖也叫共享-獨佔鎖。當讀寫鎖以讀模式鎖住時,它是以共享模式鎖住的;當它以寫模式鎖住時,它是以獨佔模式鎖住的。寫獨佔、讀共享。

         讀寫鎖非常適合於對資料結構讀的次數遠大於寫的情況。

  敲重點了,記住12個字:寫獨佔、讀共享;寫鎖優先順序高。

  2、主要函式  

   pthread_rwlock_init函式     //初始化

         pthread_rwlock_destroy函式  //銷燬鎖

         pthread_rwlock_rdlock函式    //讀加鎖,阻塞

         pthread_rwlock_wrlock函式   //寫解鎖,阻塞

         pthread_rwlock_tryrdlock函式  //嘗試讀解鎖

         pthread_rwlock_trywrlock函式 //嘗試寫加鎖

         pthread_rwlock_unlock函式  //解鎖

  以上7 個函式的返回值都是:成功返回0, 失敗直接返回錯誤號。 

        pthread_rwlock_t型別   用於定義一個讀寫鎖變數。

        pthread_rwlock_t rwlock;

  這些參考互斥鎖的函式,進行對比學習,只是多了讀鎖和寫鎖,就不過多解釋了。

  例項程式,3個執行緒“寫”全域性變數,5個全域性變數“讀”全域性變數,程式如下:

/* 3個執行緒不定時 "寫" 全域性資源,5個執行緒不定時 "讀" 同一全域性資源 */

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

int counter;                          //全域性資源
pthread_rwlock_t rwlock;

void *th_write(void *arg)
{
    int t;
    int i = (int)arg;

    while (1) {
        t = counter;
        usleep(1000);

        pthread_rwlock_wrlock(&rwlock);
        printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
        pthread_rwlock_unlock(&rwlock);

        usleep(5000);
    }
    return NULL;
}

void *th_read(void *arg)
{
    int i = (int)arg;

    while (1) {
        pthread_rwlock_rdlock(&rwlock);
        printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
        pthread_rwlock_unlock(&rwlock);

        usleep(900);
    }
    return NULL;
}

int main(void)
{
    int i;
    pthread_t tid[8];

    pthread_rwlock_init(&rwlock, NULL);

    for (i = 0; i < 3; i++)
        pthread_create(&tid[i], NULL, th_write, (void *)i);

    for (i = 0; i < 5; i++)
        pthread_create(&tid[i+3], NULL, th_read, (void *)i);

    for (i = 0; i < 8; i++)
        pthread_join(tid[i], NULL);

    pthread_rwlock_destroy(&rwlock);            //釋放讀寫瑣

    return 0;
}
View Code

  另兩種方式,還有條件變數和訊號量,條件變數比較難理解,篇幅比較多,所以會另寫一篇部落格來寫,敬請期待哦!