執行緒模組

机械心發表於2024-05-23

概述

該模組基於pthread實現。sylar說,由於c++11中的thread也是由pthread封裝實現的,並且沒有提供讀寫互斥量,讀寫鎖,自旋鎖等,所以自己封裝了pthread。包括以下類:

  • Thread:執行緒類,建構函式傳入執行緒入口函式和執行緒名稱,執行緒入口函式型別為void(),如果帶引數,則需要用std::bind進行繫結。執行緒類構造之後執行緒即開始執行,建構函式線上程真正開始執行之後返回。

  • 執行緒同步類(這部分被拆分到mutex.h)中:

  • Semaphore: 計數訊號量,基於sem_t實現

  • Mutex: 互斥鎖,基於pthread_mutex_t實現

  • RWMutex: 讀寫鎖,基於pthread_rwlock_t實現

  • Spinlock: 自旋鎖,基於pthread_spinlock_t實現

  • CASLock: 原子鎖,基於std::atomic_flag實現

執行緒模組主要由Thread類實現

  • class Thread:實現執行緒的封裝

關於執行緒id的問題,在獲取執行緒id時使用syscall獲得唯一的執行緒id

程序pid: getpid()                 
執行緒tid: pthread_self()     //程序內唯一,但是在不同程序則不唯一。
執行緒pid: syscall(SYS_gettid)     //系統內是唯一的

鎖模組介紹

訊號量


訊號量(Semaphore)是一種用於多執行緒同步的機制,能夠控制多個執行緒對共享資源的訪問。訊號量的關鍵作用是透過計數器來管理訪問許可權,從而避免競爭條件。訊號量有兩個主要操作:

  • 等待(P 操作,wait 或 down):減少訊號量的值。如果訊號量的值為0,則呼叫執行緒將被阻塞,直到訊號量的值大於0。
  • 釋放(V 操作,signal 或 up):增加訊號量的值。如果有執行緒因為等待操作而被阻塞,則喚醒其中一個執行緒。

有以下訊號量函式:

  • sem_init(): 初始化一個未命名的訊號量。
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem: 指向訊號量物件的指標。
// pshared: 指定訊號量是否在程序間共享。0 表示訊號量在同一程序的執行緒間共享,非0表示訊號量在程序間共享。
// value: 訊號量的初始值。
  • sem_destroy(): 銷燬一個未命名的訊號量。
#include <semaphore.h>
int sem_destroy(sem_t *sem);
// sem: 指向訊號量物件的指標。
  • sem_wait(): 等待(減少)訊號量。如果訊號量的值為0,呼叫執行緒將阻塞直到訊號量的值大於0。
#include <semaphore.h>
int sem_wait(sem_t *sem);
// sem: 指向訊號量物件的指標。
  • sem_trywait(): 嘗試等待(減少)訊號量。如果訊號量的值為0,該函式立即返回,並不會阻塞。
#include <semaphore.h>
int sem_trywait(sem_t *sem);
// sem: 指向訊號量物件的指標。
  • sem_post(): 釋放(增加)訊號量。如果有其他執行緒因等待訊號量而被阻塞,該函式會喚醒其中一個執行緒。
#include <semaphore.h>
int sem_post(sem_t *sem);
// sem: 指向訊號量物件的指標。
  • sem_getvalue(): 獲取訊號量的當前值。
#include <semaphore.h>
int sem_getvalue(sem_t *sem);
// sem: 指向訊號量物件的指標。
// sval: 指向整數的指標,用於儲存訊號量的當前值。

Sylar對其進行了封裝,形成class Semaphore

// 訊號量,它本質上是一個長整型的數
sem_t m_semaphore;

// 建構函式
Semaphore::Semaphore(uint32_t count) {
    if(sem_init(&m_semaphore, 0, count)) {
        throw std::logic_error("sem_init error");
    }
}

// 解構函式
Semaphore::~Semaphore() {
    sem_destroy(&m_semaphore);
}

// 獲取訊號量
void Semaphore::wait() {
    if(sem_wait(&m_semaphore)) {
        throw std::logic_error("sem_wait error");
    }
}

// 釋放訊號量
void Semaphore::notify() {
    if(sem_post(&m_semaphore)) {
        throw std::logic_error("sem_post error");
    }
}

互斥鎖

pthread_mutex_t是Pthreads庫中的一種互斥鎖(Mutex),用於線上程間提供同步機制,確保在多執行緒環境中對共享資源的互斥訪問。相關函式:

#include <pthread.h>

pthread_mutex_t mutex;
// 初始化
pthread_mutex_init(&mutex, NULL);
// 銷燬互斥鎖
pthread_mutex_destroy(&mutex);
// 加鎖互斥鎖
pthread_mutex_lock(&mutex);
// 嘗試加鎖互斥鎖
pthread_mutex_trylock(&mutex);
// 解鎖互斥鎖
pthread_mutex_unlock(&mutex);

為方便封裝各種鎖,這裡定義了3個結構體,都在建構函式時自動lock,在析構時自動unlock,這樣可以簡化鎖的操作,避免忘記解鎖導致死鎖。

  • ScopedLockImpl:用來分裝互斥量,自旋鎖,原子鎖
  • ReadScopedLockImpl && WriteScopedLockImpl:用來封裝讀寫鎖

Sylar對其進行了封裝,形成class Mutex

// 互斥量
pthread_mutex_t m_mutex;

// 建構函式
Mutex () {
    pthread_mutex_init(&m_mutex, nullptr);
}

// 解構函式
~Mutex () {
    pthread_mutex_destroy(&m_mutex);
}

// lock(加鎖)
void lock() {
    pthread_mutex_lock(&m_mutex);
}

// unlock(解鎖)
void unlock() {
        pthread_mutex_unlock(&m_mutex);
    }

自旋鎖

與mutex不同,自旋鎖不會使執行緒進入睡眠狀態,而是在獲取鎖時進行忙等待,直到鎖可用。當鎖被釋放時,等待獲取鎖的執行緒將立即獲取鎖,從而避免了執行緒進入和退出睡眠狀態的額外開銷。

Sylar中基於pthread_spinlock_t及其相關函式封裝了class Spinlock

// 自旋鎖定義
pthread_spinlock_t m_mutex;

// 建構函式
Spinlock() {
    pthread_spin_init(&m_mutex, 0);
}

// 解構函式
~Spinlock() {
    pthread_spin_destroy(&m_mutex);
}

// 加鎖
void lock() {
    pthread_spin_lock(&m_mutex);
}

// 解鎖
void unlock() {
    pthread_spin_unlock(&m_mutex);
}

讀寫鎖

讀寫鎖是一種同步機制,用於在多執行緒環境下對共享資源進行訪問控制。與互斥鎖不同,讀寫鎖允許多個執行緒同時讀取共享資源,但只允許一個執行緒寫入共享資源。這樣可以提高程式的效能和效率,但需要注意避免讀寫鎖死鎖等問題。

Sylar基於pthread_rwlock_t及其相關函式封裝了class RWMutex

// 讀寫鎖
pthread_rwlock_t m_lock;

// RWMutex(建構函式)
RWMutex() {
    pthread_rwlock_init(&m_lock, nullptr);
}

// ~RWMutex(解構函式)
~RWMutex() {
    pthread_rwlock_destroy(&m_lock);
}

// rdlock(加讀鎖)
void rdlock() {
    pthread_rwlock_rdlock(&m_lock);
}

// wrlock(加寫鎖)
void wrlock() {
    pthread_rwlock_wrlock(&m_lock);
}

// unlock(解鎖)
void unlock() {
    pthread_rwlock_unlock(&m_lock);
}

原子鎖

在多執行緒程式設計中,原子標誌位通常用於實現簡單的鎖機制,以確保對共享資源的訪問是互斥的。使用atomic_flag.clear()可以輕鬆地重置標誌位,使之再次可用於控制對共享資源的訪問。需要注意的是,由於該函式是一個原子操作,因此可以安全地在多個執行緒之間使用,而無需擔心競態條件和資料競爭等問題

class CASLock(原子鎖)實現如下:

  • 成員變數
// 執行緒id 
pid_t m_id = -1;
// 執行緒結構
pthread_t m_thread = 0;
// 執行緒執行函式
std::function<void()> m_cb;
// 執行緒名稱
std::string m_name;
// 訊號量
Semaphore m_semaphore;
// m_mutex是一個原子布林型別,具有特殊的原子性質,可以用於實現執行緒間同步和互斥。
// volatile關鍵字表示該變數可能會被非同步修改,因此編譯器不會對其進行最佳化,而是每次都從記憶體中讀取該變數的值。
volatile std::atomic_flag m_mutex;

// CASLock(建構函式)
CASLock () {
    m_mutex.clear(); 
}

// lock(加鎖)
void lock() {
    while (std::atomic_flag_test_and_set_explicit(&m_mutex, std::memory_order_acquire));
}


// unlock(解鎖)
void unlock() {
    std::atomic_flag_clear_explicit(&m_mutex, std::memory_order_release);
}

執行緒模組

class Thread的實現

定義了兩個執行緒區域性變數用於指向當前執行緒以及執行緒的名稱。

static thread_local是C++中的一個關鍵字組合,用於定義靜態執行緒本地儲存變數。具體來說,當一個變數被宣告為static thread_local時,它會在每個執行緒中擁有自己獨立的靜態例項,並且對其他執行緒不可見。這使得變數可以跨越多個函式呼叫和程式碼塊,在整個程式執行期間保持其狀態和值不變。

需要注意的是,由於靜態執行緒本地儲存變數是執行緒特定的,因此它們的初始化和銷燬時機也與普通靜態變數不同。具體來說,在每個執行緒首次訪問該變數時會進行初始化,線上程結束時才會進行銷燬,而不是在程式啟動或執行期間進行一次性初始化或銷燬。

// 指向當前執行緒 
static thread_local Thread *t_thread = nullptr;
// 指向執行緒名稱
static thread_local std::string t_thread_name = "UNKNOW";
  • Thread(建構函式):初始化執行緒執行函式、執行緒名稱,建立新執行緒。
// thread:指向pthread_t型別的指標,用於返回新執行緒的ID。
// attr:指向pthread_attr_t型別的指標,該結構體包含一些有關新執行緒屬性的資訊。可以將其設定為NULL以使用預設值。
// start_routine:是指向新執行緒函式的指標,該函式將在新執行緒中執行。該函式必須採用一個void型別的指標作為引數,並返回一個void型別的指標。
// arg:是指向新執行緒函式的引數的指標。如果不需要傳遞引數,則可以將其設定為NULL。

Thread::Thread(std::function<void()> cb, const std::string &name)
    : m_cb(cb)
    , m_name(name) {
    if (name.empty()) {
        m_name = "UNKNOW";
    }
    int rt = pthread_create(&m_thread, nullptr, &Thread::run, this);
    if (rt) {
        SYLAR_LOG_ERROR(g_logger) << "pthread_create thread fail, rt=" << rt
                                  << " name=" << name;
        throw std::logic_error("pthread_create error");
    }
    m_semaphore.wait();
}

呼叫pthread_create函式後,將會建立一個新執行緒,並開始執行透過start_routine傳遞給它的函式。新執行緒的ID將儲存在thread指向的變數中。請注意,新執行緒將在與呼叫pthread_create函式的執行緒併發執行的情況下執行。

  • ~Thread(解構函式)
Thread::~Thread() {
    if (m_thread) {
        pthread_detach(m_thread);
    }
}
  • join(等待執行緒執行完成)
    當呼叫 pthread_join() 時,當前執行緒會阻塞,直到指定的執行緒完成執行。一旦執行緒結束,當前執行緒就會恢復執行,並且可以透過 retval 引數來獲取執行緒的返回值。如果不關心執行緒的返回值,也可以將 retval 引數設定為 NULL。成功:返回 0 表示執行緒成功退出。
// thread:要等待的執行緒ID。
// retval:指向指標的指標,用於儲存執行緒返回的值。如果不需要獲取返回值,則可以將其設定為NULL。
void Thread::join() {
    if (m_thread) {
        int rt = pthread_join(m_thread, nullptr);
        if (rt) {
            SYLAR_LOG_ERROR(g_logger) << "pthread_join thread fail, rt=" << rt
                                      << " name=" << m_name;
            throw std::logic_error("pthread_join error");
        }
        m_thread = 0;
    }
}
  • run(執行緒執行函式)

透過訊號量,能夠確保建構函式在建立執行緒之後會一直阻塞,直到run方法執行並通知訊號量,建構函式才會返回。
在建構函式中完成執行緒的啟動和初始化操作,可能會導致執行緒還沒有完全啟動就被呼叫,從而導致一些未知的問題。因此,在出建構函式之前,確保執行緒先跑起來,保證能夠初始化id,可以避免這種情況的發生。同時,這也可以保證執行緒的安全性和穩定性。

void *Thread::run(void *arg) {
    Thread *thread = (Thread *)arg;
    t_thread       = thread;
    t_thread_name  = thread->m_name;
    thread->m_id   = sylar::GetThreadId();
    pthread_setname_np(pthread_self(), thread->m_name.substr(0, 15).c_str());

    std::function<void()> cb;
    cb.swap(thread->m_cb);

    thread->m_semaphore.notify();

    cb();
    return 0;
}

總結

  • 對日誌系統的臨界資源進行互斥訪問時,使用自旋鎖而不是互斥鎖。
  1. mutex使用系統呼叫將執行緒阻塞,並等待其他執行緒釋放鎖後再喚醒它,這種方式適用於長時間持有鎖的情況。而spinlock在獲取鎖時忙等待,即不斷地檢查鎖狀態是否可用,如果不可用則一直迴圈等待,因此適用於短時間持有鎖的情況。
  2. 由於mutex會將執行緒阻塞,因此在高併發情況下可能會出現執行緒頻繁地進入和退出睡眠狀態,導致系統開銷大。而spinlock雖然不會使執行緒進入睡眠狀態,但會消耗大量的CPU時間,在高併發情況下也容易導致效能問題。
  3. 另外,當一個執行緒嘗試獲取已經被其他執行緒持有的鎖時,mutex會將該執行緒阻塞,而spinlock則會在自旋等待中消耗CPU時間。如果鎖的持有時間較短,則spinlock比mutex更適合使用;如果鎖的持有時間較長,則mutex比spinlock
  • 在建構函式中建立子程序並等待其完成執行是一種常見的技術,可以透過訊號量(Semaphore)來實現主執行緒等待子執行緒完成。
  1. 首先,在主執行緒中建立一個Semaphore物件並初始化為0。然後,在建構函式中建立子執行緒,並將Semaphore物件傳遞給子執行緒。子執行緒將執行所需的操作,並在最後使用Semaphore物件發出訊號通知主執行緒它已經完成了工作。
  2. 主執行緒在建構函式中呼叫Semaphore物件的wait方法,這會使主執行緒阻塞直到收到訊號並且Semaphore物件的計數器值大於0。當子執行緒發出訊號時,Semaphore物件的計數器值增加1,因此主執行緒可以繼續執行建構函式的剩餘部分。

相關文章