多線程編程2--線程的同步和互斥

當多個線程共享相同的內存時,需要確保每個線程看到一致的數據視圖。

如果每個線程內部的變量其他線程都不會訪問到,那麼就不存在一致性問題;
如果變量是隻讀的,那麼多個線程同時訪問它也不存在不一致性問題;
但是,一旦一個變量是可寫,當一個線程對它進行修改的時候,其他有可能對它進行讀取或者寫入操作從而導致數據不一致的問題。此時就需要同步機制來保證。

APUE上給出一個例子:

這裏寫圖片描述

由於遞增操作不是原子性的,因此不可避免的會出現上述數據不一致的問題。


1.互斥量

我們可以通過pthread庫提供的互斥接口來保護數據,確保在同一時間只有一個線程訪問這個數據。互斥量(mutex)本質上是一把鎖,當我們訪問共享資源對互斥量加鎖,訪問資源對互斥量解鎖。對互斥量加鎖後,任何試圖向對該互斥量加鎖的線程都會阻塞直到該鎖被釋放。

考慮多個線程訪問一個數據,每個線程在訪問數據之前都會先訪問該互斥量,如果該互斥量已經加鎖則阻塞,不然就對該互斥量加鎖再去訪問實際的數據。當有多線程因爲互斥量加鎖被阻塞時,一旦鎖釋放了,這些阻塞的線程都會運行起來,此時,第一個線程再去加鎖互斥量訪問數據,其他線程只能再次等待。

這裏寫圖片描述

這裏寫圖片描述

下面是一個關於引用計數的實例,其中count記爲引用對象的個數,要求是對於count++,count–必須只有一個線程能訪問,且只有當count == 0才釋放資源。

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/types.h>

typedef struct Counter
{
    int count;
    pthread_mutex_t mutex;
}Counter;

void Init(Counter* pc);
void Inc(Counter* pc);
void Dec(Counter* pc);

int main()
{

    Counter* pc = (Counter*)malloc(sizeof(Counter));
    if (pc == NULL)
        exit(-1);
    Init(pc);
    //...
    //...

    return 0;
}

void Init(Counter* pc)
{
    pc->count = 1;
    pthread_mutex_init(&pc->mutex, NULL);
}

void Inc(Counter* pc)
{
    pthread_mutex_lock(&pc->mutex);
    pc->count++;
    pthread_mutex_unlock(&pc->mutex);
}

void Dec(Counter* pc)
{
    pthread_mutex_lock(&pc->mutex);
    pc->count--;
    if (pc->count == 0)
    {
        pthread_mutex_unlock(&pc->mutex);
        pthread_mutex_destroy(&pc->mutex);
        free(pc);
    }
    else
    {
        pthread_mutex_unlock(&pc->mutex);
    }
}

關於死鎖

如果一個線程試圖對同一個互斥量加兩次鎖則會造成死鎖。當然還有其他不明顯的方式可能導致死鎖,考慮以下的情況,線程A佔有第一個互斥量,線程B佔有第二個互斥量,線程A試圖去佔有第二個互斥量而出於阻塞,線程B試圖去佔有第一個互斥量時也處於阻塞,這樣子一來,A在等待B釋放鎖,B也在等待A釋放鎖,相互阻塞,結果是誰也釋放不了鎖,造成死鎖。

這裏寫圖片描述

這裏寫圖片描述

死鎖一般要滿足四個條件:

  1. 互斥
  2. 不可搶佔
  3. 佔有並等待
  4. 循環等待

上述解除死鎖的方法實際上就是破壞這幾個其中的一個,比如用trylock函數如果不能獲得鎖就釋放鎖,這就是破壞第三個條件,而按照鎖的先後次序去得到鎖則是破壞了第四個條件。

具體可參見死鎖,銀行家算法

2.條件變量

條件變量是線程可用的另一種同步機制,條件變量和互斥量一起使用,允許線程以無競爭的方式等待特定條件的發生。

這裏寫圖片描述

這裏寫圖片描述

基於上述函數,我們寫一個生產者消費者的例子,生產者在鏈表表頭插入,消費者在鏈表頭刪除,保證鏈表頭有數據消費者才刪除,不然消費者阻塞。

代碼如下:

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

#define TIMES 5


typedef struct node
{
    int data;
    struct Node* next;
}node, *node_p, **node_pp;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condition = PTHREAD_COND_INITIALIZER;


node_p head = NULL;

node_p allocate_node(int x);

int empty(node_p head);

void init_list(node_pp head);

void push_front(node_p head, int x);

void pop_front(node_p head, int* value);

void free_node(node_p node);

void destroy_list(node_p head);

void show_list(node_p head);


void* thread1(void* args) //push_front
{
    int x = 0;
    srand(time(NULL));

    while (1)
    {
        x = rand() % 10 + 1;
        pthread_mutex_lock(&mutex);
        push_front(head, x);
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&condition);
        printf("push_front : %d\n", x);    
        sleep(1);
    }
}

void* thread2(void* args) //pop_front
{
    int x = 0;
    while (1)
    {
        pthread_mutex_lock(&mutex);
        while (empty(head))
        {
            pthread_cond_wait(&condition, &mutex);
        }
        pop_front(head, &x);
        pthread_mutex_unlock(&mutex);
        if (x != 0)
            printf("pop_front : %d\n", x);    
    }
}


int main()
{
    init_list(&head);

    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);


    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    destroy_list(head);
    return 0;
}




node_p allocate_node(int x)
{
    node_p ret = malloc(sizeof(node));
    if (ret == NULL)
    {
        perror("malloc failure");
        exit(-1);
    }

    ret->data = x;
    ret->next = NULL;
    return ret;
}

int empty(node_p head)
{
    return head->next == NULL;
}

void init_list(node_pp head)
{
    *head = allocate_node(0);
}

void push_front(node_p head, int x)
{
    node_p tmp = allocate_node(x);

    tmp->next = head->next;
    head->next = tmp;
}

void pop_front(node_p head, int* value)
{
    if (empty(head))
    {
        printf("list is empty\n");
        return;
    }
    node_p del = head->next;
    head->next = del->next;

    *value = del->data;
    free_node(del);
}

void free_node(node_p n)
{
    if (n != NULL)
    {
        free(n);
        n = NULL;
    }
}

void show_list(node_p head)
{
    head = head->next;
    while (head != NULL)
    {
        printf("%d  ", head->data);
        head = head->next;
    }
    printf("\n");
}

void destroy_list(node_p head)
{
    int data = 0;
    while (!empty(head))
    {
        pop_front(head, &data);
    }

    free_node(head);
}

3.信號量

mutex變量是二元信號量,非0即1,初始化mutex = 1表示只有一個可用資源,加鎖獲得該資源,將mutex置爲0,表示沒有資源;釋放鎖,將mutex = 1表示又有了一個資源。

信號量則可以表示有多個資源,一般sem_t數據類型來表示,具體會用到以下函數:

這裏寫圖片描述

這裏寫圖片描述

下面我們以一個例子來看下具體這些函數是如何使用的?

我們基於環形隊列實現生產者消費者模型:
//該模型需要滿足以下條件:
//1.互斥訪問,任意時刻只能有一個線程訪問臨界資源(buffer)
//2.條件同步,buffer滿時,生產者必須等待消費者;buffer空時,消費者必須等待生產者

我們設定緩衝區大小爲SIZE個,用buf_sem表示緩衝區的剩餘容量,data _sem表示緩衝區的元素個數,滿足data _sem + buf _sem == SIZE 每次生產者生成一個,則buf減小一個,對應的data增大一個;同理消費者消費一個,buf增大一個,data減小一個。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
#include<stdlib.h>
#include<time.h>

#define SIZE 32

int buffer[SIZE];//數組用來模擬環形隊列

sem_t buf_sem;
sem_t data_sem;

void* consumer(void* arg)
{
    int index = 0;
    while (1)
    {   
        sem_wait(&data_sem);
        int data = buffer[index];
        printf("consumer consumes data : %d\n", data);        
        sem_post(&buf_sem);
        index++;
        index %= SIZE;
        sleep(1);
    }
}

void* producer(void* arg)
{
    srand(time(NULL));
    int index = 0;
    while (1)
    {
        sem_wait(&buf_sem);
        int data = rand() % 100;
        buffer[index] = data;
        printf("producer produces data : %d\n", data);
        sem_post(&data_sem);
        index++;
        index %= SIZE;
    }
}



int main()
{
    sem_init(&buf_sem, 0, SIZE);//buf_sem初始化爲buf的大小
    sem_init(&data_sem , 0, 0);//data_sem初始化爲數據的個數,開始爲0

    pthread_t c, p;

    //Create threads
    pthread_create(&c, NULL, consumer, NULL);
    pthread_create(&p, NULL, producer, NULL);


    pthread_join(c, NULL);
    pthread_join(p, NULL);

    sem_destroy(&buf_sem);
    sem_destroy(&data_sem);
    return 0;
}

注意到sem_ init (&buf_sem, 0, SIZE); 第二參數我們設爲0,這是因爲我們實現的線程之間的同步,具體我們參見手冊:

int sem_init(sem_t *sem, int pshared, unsigned int value);

The pshared argument indicates whether this semaphore is to be shared between the threads of a process, or between processes.

       If  pshared  has the value 0, then the semaphore is shared between the threads of a process, and should be located at some address that is visible to all threads (e.g., a global variable, or a variable allocated dynamically on the heap).

       If pshared is nonzero, then the semaphore is shared between processes, and should be located in a region of shared memory (see shm_open(3), mmap(2), and  shmget(2)).   (Since  a child created by fork(2) inherits its parent's memory mappings, it can also access the semaphore.)  Any process that can access the shared memory region can operate on the semaphore using sem_post(3), sem_wait(3), and so on.

4.讀寫鎖

讀寫鎖和互斥量有點類似,但是它允許更高的並行性。在很多情況下,有些公共數據其實修改的機會很少,他們讀的機會很多,也就是讀多少寫,我們知道多個線程讀取數據時並不會導致數據的不一致性,這時候如果在讀時候加鎖是會降低我們程序的效率。

因此,針對這種多讀少寫的情況,我們引入了讀寫鎖。讀寫鎖將共享資源的訪問者分爲讀者和寫者,它有以下特點:

  1. 允許多個讀者讀取數據(讀者最多是實際的CPU數)
  2. 只允許一個寫者寫入數據
  3. 保證任意時刻讀者和寫者不能共存

讀寫鎖也叫共享-獨佔鎖,共享性是針對讀模式的,獨佔性是針對寫模式的。

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

爲了演示如何使用這些函數,我們定義一個全局變量data,保證三個線程讀,三個線程寫,看看利用讀寫鎖是什麼樣的結果?

/*************************************************************************
    > File Name: lock.c
    > Author: xuyang
    > Mail: [email protected] 
    > Created Time: 2017-06-14 22:03:05
 ************************************************************************/

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

#define READERS 2
#define WRITERS 2

int data = 0;

pthread_rwlock_t rwlock;


void* do_read(void* arg)
{
    pthread_detach(pthread_self());
    while (1)
    {
        if (pthread_rwlock_rdlock(&rwlock) != 0)
        {
            printf("writer is writing, reader waiting\n");
        }
        else
        {
            printf("reader %d : data read is %d\n", (int)arg, data);
            pthread_rwlock_unlock(&rwlock);
        }
        sleep(1);
    }

}

void* do_write(void* arg)
{
    pthread_detach(pthread_self());
    while (1)
    {
        if (pthread_rwlock_wrlock(&rwlock) != 0)
        {
            printf("reader is reading , writer waiting\n");
        }
        else
        {
            data++;
            printf("writer %d : data write is %d\n", (int)arg, data);
            pthread_rwlock_unlock(&rwlock);
        }
        sleep(5);
    }
}


int main()
{

    pthread_rwlock_init(&rwlock, NULL);

    pthread_t pid;
    int i = 0;
    for (; i < READERS; i++)
    {
        pthread_create(&pid, NULL, do_read, (void*)i);
    }

    int j = 0;
    for (; j < WRITERS; i++)
    {
        pthread_create(&pid, NULL, do_write, (void*)j);
    }
    return 0;
}
發佈了96 篇原創文章 · 獲贊 119 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章