線程同步機制——POSIX信號量、互斥量、條件變量

信號量

#include<semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destory(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_post(sem_t* sem);

這些函數的第一個參數sem指向被操作的信號量。

  • sem_init函數用於初始化一個未命名的信號量。pshared參數指定信號量的類型。如果其值爲0,就表示這個信號量是當前進程的局部信號量,否則該信號量就可以在多個進程之間共享。value參數指定信號量的初始值。此外,初始化一個已經被初始化的信號量,將會導致不可預期得結果
  • sem_destory函數用於銷燬信號量,以釋放其佔用的內核資源。如果銷燬一個正在被其他線程等待的信號量,將會導致不可預期的結果。
  • sem_wait函數以原子操作的方式將信號量的值減1.如果信號量的值爲0,則sem_wait將被阻塞,直到這個信號量具有非0值。
  • sem_trywait函數與sem_wait函數相似,不過它始終立即返回,而不論被操作的信號量是否具有非0值,相當於sem_wait的非阻塞版本。當前信號量的值非0時,sem_trywait對信號量執行減1操作。當信號量的值爲0時,它將返回-1並設置errno爲EAGAIN。
  • sem_post函數以原子操作的方式將信號量的值加1。當信號量的值大於0時,其他正在調用sem_wait等待信號量的線程將被喚醒。
    上面這些函數成功時返回0,失敗則返回-1,並設置errno。

一個初始值爲N的信號量允許N個線程併發訪問。線程訪問資源的時候首先獲取信號量,進行如下操作

  • 將信號量的值減-1 (sem_wait函數)
  • 如果信號量的值小於0,則進入等待狀態,否則繼續執行。
    當訪問完資源後,線程釋放信號量,進行如下操作。
  • 將信號量的值加1(sem_post函數).
  • 如果信號量的值小於1,喚醒一個等待的線程。

互斥鎖

互斥鎖也稱互斥量,可以用於保護關鍵代碼段,以確保其獨佔式的訪問。當進入關鍵代碼段時,我們需要獲得互斥鎖並將其加鎖;當離開代碼段時,我們需要對互斥鎖解鎖,以喚醒其他等待該互斥量的線程。

互斥鎖基礎API
#include<pthread.h>
int phtread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* mutexattr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int phthread_mutex_lock(pthread_mutex_t* mutex);
int phtread_mutex_trylock(phtread_mutex_t* mutex);
int phtread_mutex_unlock(phtread_mutex* mutex);

這些函數的第一個參數mutex指向要操作的目標互斥鎖,互斥鎖的類型時pthread_mutex_t結構體。

  • pthread_mutex_init函數用於初始化互斥鎖。mutexattr參數指定互斥鎖的屬性。如果將它設置爲NULL,則表示使用默認屬性。我們還可以用如下方式來初始化一個互斥鎖:
pthread_mutex_t mutex = PHTREAD_MUTEX_INITIALIZER;
//宏PHTREAD_MUTEX_INITIALIZER把互斥鎖的各字段都初始化爲0.
  • pthread_mutex_lock函數以原子操作的方式給一個互斥鎖加鎖。如果目標互斥鎖已經被加鎖,則該函數調用將阻塞住,直到該互斥鎖的佔有者將其解鎖
  • pthread_mutex_trylock與上條函數類似,不過它始終立即返回,而不論被操作的互斥鎖是否已經被加鎖,相當於pthread_mutex_lock函數的非阻塞版本。當目標互斥鎖未被加鎖時,則將其進行加鎖。當互斥鎖已經被加鎖時,該函數返回錯誤碼EBUSY。
  • pthread_mutex_unlock函數以原子操作的方式給一個互斥鎖解鎖。如果此時有其他線程正在等待這個互斥鎖,則這些線程中的某一個將獲得它.
    上面這些函數成功時返回0, 失敗則返回錯誤碼。

死鎖情況舉例

//死鎖情況
#include<pthread.h>
#include<iostream>
#include<unistd.h>

int a = 0;
int b = 0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;

void* anthor(void* arg)
{
    pthread_mutex_lock(&mutex_b);
    std::cout << "in child thread, got mutex b, waiting for mutex a\n";
    sleep(5);
    ++b;
    pthread_mutex_lock(&mutex_a);
    b += a++;
    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);
    pthread_exit(NULL);
}

int main()
{
    pthread_t id;
    //初始化鎖
    pthread_mutex_init(&mutex_a, NULL);
    pthread_mutex_init(&mutex_b, NULL);
    //創建線程
    pthread_create(&id, NULL, anthor, NULL);
    
    pthread_mutex_lock(&mutex_a);
    std::cout << "in parent thread, got mutex a, waiting for mutex b\n";
    sleep(5);
    ++a;
    pthread_mutex_lock(&mutex_b);
    a += b++;
    pthread_mutex_unlock(&mutex_a);
    pthread_mutex_unlock(&mutex_b);

    //等待線程id結束
    pthread_join(id, NULL);
    //銷燬鎖
    pthread_mutex_destroy(&mutex_a);
    pthread_mutex_destroy(&mutex_b);
}

在這裏插入圖片描述
該程序運行結果,造成死鎖,程序一直鎖住無法繼續執行。

我們來分析一下代碼:


主線程試圖先佔有互斥鎖mutex_a,然後操作該鎖保護的變量a,但操作完畢之後,主線程並沒有立即釋放互斥鎖mutex_a,而是又申請互斥鎖mutex_b,並在兩個互斥鎖的保護下,操作變量a和b,最後才一起釋放兩個互斥鎖;與此同時,子線程則按照相反的順序來申請互斥鎖mutex_a和mutex_b,並在兩個鎖的保護下操作變量a和b。我們用sleep函數來模擬連續兩次調用pthread_mutex_lock之間的時間差,以確保代碼中的兩個線程各自先佔有一個互斥鎖(主線程佔有互斥鎖a,子線程佔有互斥鎖b),然後等待另一個互斥鎖。這樣,兩個線程就僵持住了,誰也不能繼續往下執行,從而形成死鎖。如果代碼不加入sleep函數,則這段代碼有可能會成功運行,但是這種邏輯是一個典型的死鎖錯誤。


條件變量

如果說互斥鎖是用於同步線程對共享數據的訪問的話,那麼條件變量則是用於在線程之間同步共享數據的值。條件變量提供了一種線程間的通知機制;當某個共享數據達到某個值的時候,喚醒等待這個共享數據的線程。
條件變量相關函數主要有以下五個

#include<pthread.h>
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* cond_attr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
int phtread_cond_wait(pthread_cond_t* cond, phtread_mutex_t* mutex);

這些函數的第一個參數cond指向要操作的目標條件變量,條件變量的類型是pthread_cond_t結構體。

  • pthread_cond_init函數用於初始化條件變量。cond_attr參數指定條件變量的屬性。如果將它設置爲NULL,則表示使用默認屬性。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //另一種初始化方式
  • phtread_cond_destroy函數用於銷燬條件變量,以釋放其佔有的內核資源。銷燬一個正在被等待的條件變量將失敗返回EBUSY。
  • pthread_cond_broadcast函數以廣播的方式喚醒所有等待目標條件變量的線程。
  • pthread_cond_signal函數用於喚醒一個等待目標條件變量的線程。至於哪個線程將被喚醒,則取決於線程的優先級和調度策略。有時候我們可能想喚醒一個指定的線程,但pthread沒有對該需求提供解決方法。不過我們可以間接地實現需求:定義一個能夠唯一表示目標線程的全局變量,在喚醒等待條件變量的線程前先設置該變量爲目標線程,然後採用廣播方式喚醒所有等待條件變量的線程,這些線程被喚醒後都檢查該變量以判斷被喚醒的是否是自己,如果是就開始執行後續代碼,如果不是則返回繼續等待。
  • pthread_cond_wait函數用於等待目標條件變量。mutex參數是用於保護條件變量的互斥鎖,**以確保pthread_cond_wait操作的原子性。**在調用pthread_cond_wait前,必須確保互斥鎖mutex已經加鎖,否則將導致不可預期的結果。pthread_cond_wait函數執行時,**首先把調用線程放入條件變量的等待隊列中,然後將互斥鎖mutex解鎖。**可見,從pthread_cond_wait開始執行到其調用線程被放入條件變量的等待隊列之間的這段時間內,pthread_cond_signal和pthread_cond_broadcast等函數不會修改條件變量。換言之就是pthread_cond_wait函數不會錯過目標條件變量的任何變化。當pthread_cond_wait函數成功返回時,互斥鎖mutex將再次被鎖上。
    上面這些函數成功時返回0,失敗則返回錯誤碼
在生產者消費者模型中,如果我們的線程此時都去處理任務,這個時候有新的任務加入而沒有線程在等待,此時生產者調用pthread_cond_signal函數會產生什麼?

答:條件變量並不保存狀態信息,只是傳遞應用程序狀態信息的一種通訊機制。發送信號時,若無任何線程在等待該條件變量,這個信號也就會不了了之。線程如在此後等待該條件變量,只有當再次收到次變量的下一個信號時,方可解除阻塞狀態。

我們將在下面代碼中將以上三種線程同步機制封裝爲三個類。

//對信號量、互斥鎖和條件變量封裝爲三個類
#ifndef LOCKER_H
#define LOCKER_H
#include<iostream>
#include<exception>
#include<pthread.h>
#include<semaphore.h>

//封裝信號量的類
class sem
{
public:  
    //創建並初始化信號量
    sem()
    {
        if(sem_init(&m_sem, 0, 0) != 0)
        {
            //構造函數沒有返回值,可以通過拋出異常來報告錯誤
            throw std::exception();
        }
    }
    //銷燬信號量
    ~sem()
    {
        sem_destroy(&m_sem);
    }
    //等待信號量
    bool wait()
    {
        return sem_wait(&m_sem) == 0;
    }
    //增加信號量
    bool post()
    {
        return sem_post(&m_sem) == 0;
    }

private:  
    sem_t m_sem;
};

//封裝互斥鎖的類
class locker
{
public:  
    //創建並初始化互斥鎖
    locker()
    {
        if(pthread_mutex_init(&m_mutex, NULL) != 0)
            throw std::exception();
    }
    //銷燬互斥鎖
    ~locker()
    {
        pthread_mutex_destroy(&m_mutex);
    }
    //獲取互斥鎖
    bool lock()
    {
        return pthread_mutex_lock(&m_mutex) == 0;
    }

    //釋放互斥鎖
    bool unlock()
    {
        return pthread_mutex_unlock(&m_mutex) == 0;
    }

private:  
    pthread_mutex_t m_mutex;
};


//封裝條件變量的類
class cond
{
public:  
    //創建並初始化條件變量
    cond()
    {
        if(pthread_mutex_init(&m_mutex, NULL) != 0)
            throw std::exception();
        if(pthread_cond_init(&m_cond, NULL) != 0)
            throw std::exception();
    }
    //銷燬條件變量
    ~cond()
    {
        pthread_mutex_destroy(&m_mutex);
        pthread_cond_destroy(&m_cond);
    }
    //等待條件變量
    bool wait()
    {
        int ret = 0;
        pthread_mutex_lock(&m_mutex);
        ret = pthread_cond_wait(&m_cond, &m_mutex);
        pthread_mutex_unlock(&m_mutex);
        return ret == 0;
    }
    //喚醒等待條件變量的線程
    bool signal()
    {
        return pthread_cond_signal(&m_cond) == 0;
    }
    
private:  
    pthread_mutex_t m_mutex;
    pthread_cond_t m_cond;
};

下面是我們用上面封裝的鎖和信號量實現的線程池結構

#ifndef THREADPOOL_H
#define THREADPOOL_H
#include<list>
#include<cstdio>
#include<exception>
#include<pthread.h>
#include"locker.h"

//線程池類,將它定義爲模板類是爲了代碼複用。模板參數T是任務類
template<typename T>
class threadpool
{
public:  
    /*參數thread_number是線程池中線程的數量,max_requests是請求隊列中最多允許的、等待處理請求的數量 */
    threadpool(int thread_number = 8, int max_requests = 10000);
    ~threadpool();
    //往請求隊列中添加任務
    bool append(T* request);

    //工作線程運行函數
    static void* worker(void* arg);
    void run();

private:  
    int m_thread_number; //線程池中的線程數
    int m_max_requests; //請求隊列中允許的最大請求數
    pthread_t* m_threads; //描述線程池的數組,其大小爲m_thread_number
    std::list<T*>m_workqueue; //請求隊列
    locker m_queuelocker; //保護請求隊列的互斥鎖
    sem m_queuestat; //是否有任務需要處理
    bool m_stop; //是否結束線程
};

template<typename T>
threadpool<T>::threadpool(int thread_number, int max_requests)
        : m_thread_number(thread_number), m_max_requests(max_requests), m_stop(false), m_threads(NULL)
{
    if((thread_number <= 0) || (max_requests <= 0) )
        throw std::exception();
    //創建線程池
    m_threads = new pthread_t[m_thread_number];
    if(!m_threads)
    {
        throw std::exception();
    }
    //創建thread_number個線程,並將它們設置爲脫離線程
    for(int i = 0; i < m_thread_number; ++i)
    {
        printf("create the %dth thread\n", i);
        if(pthread_create(m_threads + i, NULL, worker, this) != 0)
        {
            delete [] m_threads;
            throw std::exception();
        }
        if(pthread_detach(m_threads[i]))
        {
            delete [] m_threads;
            throw std::exception();
        }
    }            
}
            
template<typename T>  
threadpool<T>::~threadpool()
{
    delete [] m_threads;
    m_stop = true;
}

//將任務添加到任務隊列中
template<typename T>  
bool threadpool<T>::append(T* request)
{
    //操作工作隊列是一定要加鎖,因爲它被所有線程共享
    m_queuelocker.lock();
    if(m_workqueue.size() > m_max_requests)
    {
        m_queuelocker.unlock();
        return false;
    }
    m_workqueue.push_back(request);
    m_queuelocker.unlock();
    //將信號量加1,喚醒等待線程
    m_queuestat.post();
    return true;
}

template<typename T> 
void* threadpool<T>::worker(void* arg)
{
    threadpool* pool = (threadpool*)arg;
    pool->run();
    return pool;
}

template<typename T>  
void threadpool<T>::run()
{
    while(!m_stop)
    {
        //信號量減1,阻塞等待被喚醒
        m_queuestat.wait();
        m_queuelocker.lock();
        //如果任務隊列中無任務
        if(m_workqueue.empty())
        {
            //解鎖
            m_queuelocker.unlock();
            continue;
        }
        //將任務隊列頭取出
        T* request = m_workqueue.front();
        //將取出的任務從隊列中刪掉
        m_workqueue.pop_front();
        ////
        m_queuelocker.unlock();
        if(!request)
        {
            continue;
        }
        request->process(); //執行回調函數,該函數未定義,根據大家的需求自己定義
    }
}
#endif
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章