本篇主要講Linux環(huán)境下的多線程同步內(nèi)核對象。
(1)linux線程同步之互斥體:linux互斥體的用法與windows的臨界區(qū)對象類似,使用數(shù)據(jù)結(jié)構(gòu) pthread_mutex_t表示互斥體對象(定義于pthread.h頭文件中),初始化方式有兩種:
? ? ? ? ? ? ? 1.使用 PTHREAD_MUTEX_INITIALIZER 直接給互斥體變量賦值,如:pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
? ? ? ? ? ? ? 2.使用pthread_mutex_init函數(shù)初始化,如果互斥量是動態(tài)分配的或者需要給互斥量設(shè)置屬性,函數(shù)簽名:
? ? ? ? ? ? ? ? ? int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr); ?
? ? ? ? ? ? ? ? //參數(shù) mutex 即我們需要初始化的 mutex 對象的指針,參數(shù) attr 是需要設(shè)置的互斥體屬性,設(shè)置NULL表示默認(rèn)屬性PTHREAD_MUTEX_NORMAL(普通鎖),此外還有
PTHREAD_MUTEX_ERRORCHECK(檢錯鎖),PTHREAD_MUTEX_RECURSIVE(嵌套鎖),普通鎖則是線程獨占,有線程占用的情況下其他線程調(diào)用上鎖函數(shù)阻塞,檢錯鎖是已
經(jīng)上鎖的線程對互斥體對象重復(fù)加鎖,上鎖返回EDEADLK,允許同一個線程對其持有的互斥體重復(fù)加鎖,每加鎖一次互斥體對象的鎖引用就會新增一次,解鎖會減少一次,當(dāng)計數(shù)為0時
其他線程才能獲得該鎖。
? ? ? ? ? ? ?? 互斥體對象銷毀:int pthread_mutex_destroy(pthread_mutex_t* mutex); 成功則返回0 ,注意:使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量無須銷毀,要去銷毀一
個已經(jīng)加鎖或正在被條件變量使用的互斥體對象。
? ? ? ? ? ? ?? 對于互斥體的加鎖和解鎖操作一般使用以下三個函數(shù):
? ? ? ? ? ? ? ? int ? pthread_mutex_lock(pthread_mutex_t* mutex);
? ? ? ? ? ? ? ? int ? pthread_mutex_trylock(pthread_mutex_t* mutex);
? ? ? ? ? ? ? ? int ? pthread_mutex_unlock(pthread_mutex_t* mutex);
? ? ? ? ? ?? 使用 pthread_mutexattr_settype/pthread_mutexattr_gettype 設(shè)置或獲取想要的屬性類型:
? ? ? ? ? ? ? ? int? pthread_mutexattr_settype(pthread_mutexattr_t* attr,int type);
? ? ? ? ? ? ? ? int? pthread_mutexattr_gettype(const pthread_mutexattr_t* restrictattr,int* restricttype);
如下使用例子:
? ? ? ? ? ? ? ? ?? #include<pthread.h>
? ? ? ? ? ? ? ? ? #include<stdio.h>
? ? ? ? ? ? ? ? ? #include<errno.h>?
? ? ? ? ? ? ? ? ? int? main(){
? ? ? ? ? ? ? ? ? ? ?? pthread_mutex_t mymutex;
? ? ? ? ? ? ? ? ? ? ?? pthread_mutex_init(&mymutex, NULL);
? ? ? ? ? ? ? ? ? ? ?? int ret = pthread_mutex_lock(&mymutex);
? ? ? ? ? ? ? ? ? ? ?? ret = pthread_mutex_destroy(&mymutex);
? ? ? ? ? ? ? ? ? ? ? ?? if (ret != 0)
? ? ? ? ? ? ? ? ? ? ? ?? {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? if (errno == EBUSY)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? printf("EBUSY\n");
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? printf("Failed to destroy mutex.\n");
? ? ? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? ret = pthread_mutex_unlock(&mymutex);
? ? ? ? ? ? ? ? ? ? ? ? ret = pthread_mutex_destroy(&mymutex);
? ? ? ? ? ? ? ? ? ? ? ?? if (ret == 0)
? ? ? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? printf("Succeed to destroy mutex.\n");
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? ? return 0;
? ? ? ? ? ? ? ?? }
(2)linux線程同步之信號量:與windows的Semaphore 對象使用原理一樣,linux的信號量也可以資源多份,可同時被多個線程訪問,頭文件semaphore.h,常用的一組API函數(shù):
? ? ? ? int? sem_init(sem_t* sem,int pshared,unsigned int value); //初始化信號量,參數(shù) sem 傳入初始化信號量地址;參數(shù) pshared表示該信號量是否可以被初始化該信號量的進(jìn)程 fork 出
來的子進(jìn)程共享,取值為 0 (不可以共享)、1(可以共享);參數(shù) value 用于設(shè)置信號量初始狀態(tài)下資源的數(shù)量;初始化成功返回0,失敗返回-1;
? ? ? ? int? sem_destroy(sem_t* sem); //銷毀信號量
? ? ? ? int? sem_post(sem_t* sem);? //將信號量的資源計數(shù)加一,并解鎖sem_wait而阻塞的線程
? ? ? ? int? sem_wait(sem_t* sem);? //?如果當(dāng)前信號量資源計數(shù)為 0,函數(shù)會阻塞調(diào)用線程;直到信號量對象的資源計數(shù)大于 0 時被喚醒,喚醒后將資源計數(shù)遞減 1,然后立即返回
? ? ? ? int? sem_trywait(sem_t* sem); //sem_wait函數(shù)的非阻塞版,當(dāng)前信號量對象的資源計數(shù)等于 0,函數(shù)會立即返回不會阻塞調(diào)用線程,返回值是 ﹣1,錯誤碼 errno 被設(shè)置成 EAGAIN
? ? ? ? int? sem_timedwait(sem_t* sem,conststructtimespec* abs_timeout); // 帶有等待時間的版本,等待時間在第二個參數(shù) abs_timeout 中設(shè)置,不能設(shè)置為NULL,否則會奔潰
? ? ?? 注意:1.sem_wait、sem_trywait、sem_timedwait 函數(shù)將資源計數(shù)遞減一時會同時鎖定信號量對象,因此當(dāng)資源計數(shù)為 1 時,如果有多個線程調(diào)用 sem_wait 等函數(shù)等待該信號量
時,只會有一個線程被喚醒。當(dāng) sem_wait 函數(shù)返回時,會釋放對該信號量的鎖。
? ? ? ? ? ? ? ?? 2.sem_wait、sem_trywait、sem_timedwait 函數(shù)調(diào)用成功后返回值均為 0,調(diào)用失敗返回 ﹣1,可以通過錯誤碼 errno 獲得失敗原因。
? ? ? ? ? ? ? ?? 3.sem_wait、sem_trywait、sem_timedwait 可以被 Linux 信號中斷,被信號中斷后,函數(shù)立即返回,返回值是 ﹣1,錯誤碼 errno 為 EINTR。
(3)linux線程同步之條件變量:為了讓條件變量和互斥對象兩者為原子操作,則兩者需要同時結(jié)合使用,否則會出現(xiàn)CPU的調(diào)度導(dǎo)致線程獲得互斥對象但是卻錯過了條件變量喚醒的信
號導(dǎo)致線程阻塞。條件變量常用API函數(shù)如下:
? ? ? ? int? pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr); //初始化函數(shù) 也可使用 pthread_cond_t cond = PTHREAD_COND_INITIALIZER代替;
? ? ? ? int? pthread_cond_destroy(pthread_cond_t* cond);? //銷毀函數(shù)
? ? ? ? int? pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t* restrict mutex); //條件變量不滿足,線程阻塞
? ? ? ? int? pthread_cond_timedwait(pthread_cond_t* restrictcond,pthread_mutex_t* restrictmutex,const struct timespec* restrict abstime); //等待的非租塞函數(shù),指定時間返回
? ? ? ? int? pthread_cond_signal(pthread_cond_t* cond); // 一次喚醒一個線程,具體哪個線程視情況而定,返回值非0為失敗
? ? ? ? int? pthread_cond_broadcast(pthread_cond_t* cond);? //廣播喚醒,一次喚醒多個線程 ,返回值非0為失敗
? ? ? 注意:1.當(dāng) pthread_cond_wait 函數(shù)阻塞時,它會釋放其綁定的互斥體,并阻塞線程,因此在調(diào)用該函數(shù)前應(yīng)該對互斥體有個加鎖操作,當(dāng)收到條件信號時, pthread_cond_wait?
會返回并對其綁定的互斥體進(jìn)行加鎖,因此在其下面一定有個對互斥體進(jìn)行解鎖的操作。
? ? ? ? ? ? ? ? 2.條件變量的虛假喚醒:操作系統(tǒng)可能會在一些情況下喚醒條件變量,即使沒有其他線程向條件變量發(fā)送信號,等待此條件變量的線程也有可能會醒來。pthread_cond_wait?
是 futex 系統(tǒng)調(diào)用,屬于阻塞型的系統(tǒng)調(diào)用,當(dāng)系統(tǒng)調(diào)用被信號中斷的時候,會返回 ﹣1,并且把 errno 錯誤碼置為 EINTR。很多這種系統(tǒng)調(diào)用為了防止被信號中斷都會重啟系統(tǒng)調(diào)用
(即再次調(diào)用一次這個函數(shù))。
? ? ? ? ? ? ? 3.如果一個條件變量信號條件產(chǎn)生時(調(diào)用 pthread_cond_signal 或pthread_cond_broadcast),沒有相關(guān)的線程調(diào)用 pthread_cond_wait 捕獲該信號,那么該信號條件就
會永久性地丟失了,再次調(diào)用 pthread_cond_wait 會導(dǎo)致永久性的阻塞,因此,一定要確保等待的線程在產(chǎn)生條件變量信號的線程發(fā)送條件信號之前調(diào)用 pthread_cond_wait。?
(4) linux線程同步之讀寫鎖:讀寫鎖在 Linux 系統(tǒng)中使用類型 pthread_rwlock_t 表示,?
? ? ? ? ? ? 讀鎖用于共享模式:
? ? ? ? ?? 如果當(dāng)前讀寫鎖已經(jīng)被某線程以讀模式占有了,其他線程調(diào)用pthread_rwlock_rdlock(請求讀鎖)會立刻獲得讀鎖;
? ? ? ? ?? 如果當(dāng)前讀寫鎖已經(jīng)被某線程以讀模式占有了,其他線程調(diào)用pthread_rwlock_wrlock(請求寫鎖)會陷入阻塞。
? ? ? ? ?? 寫鎖用的是獨占模式:
? ? ? ? ? 如果當(dāng)前讀寫鎖被某線程以寫模式占有,無論調(diào)用pthread_rwlock_rdlock還是pthread_rwlock_wrlock都會陷入阻塞,即寫模式下不允許任何讀鎖請求通過,也不允許任何寫鎖
請求通過,讀鎖請求和寫鎖請求都要陷入阻塞,直到線程釋放寫鎖。
? ? ? ? ?? 讀寫鎖初始化和銷毀的API:
? ? ? ? ?? int? pthread_rwlock_init(pthread_rwlock_t* rwlock,const pthread_rwlockattr_t* attr); //參數(shù) rwlock 即需要初始化和銷毀的讀寫鎖對象的地址,參數(shù) attr 用于設(shè)置讀寫鎖的屬性,設(shè)置
NULL表示默認(rèn)屬性,返回非0 為失敗,當(dāng)不需要動態(tài)創(chuàng)建或者設(shè)置非默認(rèn)屬性的讀寫鎖對象,可使用?pthread_rwlock_t myrwlock = PTHREAD_RWLOCK_INITIALIZER; 初始化
? ? ? ? ?? int? pthread_rwlock_destroy(pthread_rwlock_t* rwlock); //銷毀讀寫鎖
? ? ? ? ? 請求讀鎖和寫鎖API數(shù):
? ? ? ? ?? int? pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
? ? ? ? ?? int? pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
? ? ? ? ?? int? pthread_rwlock_timedrdlock(pthread_rwlock_t* rwlock,conststructtimespec* abstime);
? ? ? ? ?? int? pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
? ? ? ? ?? int? pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
? ? ? ? ?? int? pthread_rwlock_timedwrlock(pthread_rwlock_t* rwlock,conststructtimespec* abstime);
? ? ? ? ? 無論是讀鎖還是寫鎖,鎖的釋放都是一個接口:
? ? ? ? ? int? pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
? ? ? ? ? 讀寫鎖的屬性類型是 pthread_rwlockattr_t ,glibc 引入了如下接口來查詢和改變讀寫鎖的類型:
? ? ? ? ? int? pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr,intpref);
? ? ? ? ? int? pthread_rwlockattr_getkind_np(constpthread_rwlockattr_t* attr,int* pref);
? ? ? ? ? pthread_rwlockattr_setkind_np 的第二個參數(shù) pref 即設(shè)置讀寫鎖的類型,其取值有如下幾種:
? ? ? ? enum{
? ? ? ? PTHREAD_RWLOCK_PREFER_READER_NP, ? //讀者優(yōu)先(即同時請求讀鎖和寫鎖時,請求讀鎖的線程優(yōu)先獲得鎖)?
? ? ? ? PTHREAD_RWLOCK_PREFER_WRITER_NP, //不要被名字所迷惑,也是讀者優(yōu)先
? ? ?? PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP,? ? //寫者優(yōu)先(即同時請求讀鎖和寫鎖時,請求寫鎖的線程優(yōu)先獲得鎖) ? ? ? ? ? ?
? ? ? ? PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP // 默認(rèn),讀者優(yōu)先
? ? ? ? };
? ? ?? 初始化和銷毀 pthread_rwlockattr_t 對象:
? ? ?? int? pthread_rwlockattr_init(pthread_rwlockattr_t* attr);
? ? ?? in? tpthread_rwlockattr_destroy(pthread_rwlockattr_t* attr);
? ? ?? 初始化一個寫者優(yōu)先的讀寫鎖的例子:
? ? ?? pthread_rwlockattr_t attr;
? ? ?? pthread_rwlockattr_init(&attr);
? ? ?? pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);pthread_rwlock_t rwlock;
? ? ?? pthread_rwlock_init(&rwlock, &attr);
(5)C++11/14/17線程資源同步對象:C/C++提供的直接使用操作系統(tǒng)的功能函數(shù)雖然強(qiáng)大,也存在功能限制,同樣的代碼不能通知兼容linux和windows,因此C++11以及后續(xù)更新封裝
同步輔助類std::mutex、std::condition_variable、std::lock_guard、std::unique_lock,極大的方便了跨平臺開發(fā)。
? ? ? ?? 常用的比如 std::mutex (C++11互斥對象)、std::shared_mutex(C++17共享的互斥量),均提供了加鎖(lock)、嘗試加鎖(trylock)和解鎖(unlock)的方法,為了避免死鎖,例
如std::mutex.lock() 和 std::mutex::unlock() 方法需要成對使用,為了防止函數(shù)出口過多導(dǎo)致加鎖后沒有解鎖導(dǎo)致死鎖,推薦 RAII 技術(shù)封裝加鎖和解鎖的兩個接口,同時C++11也提供如
下封裝:
? ? ? ?? 互斥量管理 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? 版本 ? ? ? ? ? ? ? ? ? ? ? ?? 作用
? ? ? ? lock_guard ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? C++11 ? ? ? ? ? ? ? ?? 基于作用域的互斥量管理
? ? ? ? unique_lock ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? C++11 ? ? ? ? ? ? ? ? ? 更加靈活的互斥量管理
? ? ? ? shared_lock ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? C++14 ? ? ? ? ? ? ? ? ?? 共享互斥量的管理
? ? ? ? scope_lock ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? C++17 ? ? ? ? ? ? ? ? ?? 多互斥量避免死鎖的管理
? ? ? ? 注意:1.比如? void func(){
? ? ? ? ? ? ? ? ? ? ? ? ? ? std::lock_guard<std::mutex> guard(mymutex);
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //在這里放被保護(hù)的資源操作
? ? ? ? ? ? ? ? ? ? ? ? ?? }?
? ? ? ?? mymutex 的類型是 std::mutex,在 guard 對象的構(gòu)造函數(shù)中,會自動調(diào)用 mymutex.lock() 方法加鎖,當(dāng)該函數(shù)出了作用域后,調(diào)用 guard 對象時析構(gòu)函數(shù)時會自動調(diào)用 ?
mymutex.unlock() 方法解鎖 ,因此 mymutex 生命周期必須長于函數(shù) func 的作用域。
? ? ? ? ? ? ? ? ? 2.重復(fù)加鎖可能會造成程序奔潰,如果一個 std::mutex 對象已經(jīng)調(diào)用了 lock() 方法,再次調(diào)用時,其行為是未定義的,“行為未定義”即在不同平臺上可能會有不同的行為。
? ? ? ? std::condition_variable表示條件變量,與linux環(huán)境下的原生條件變量一樣,提供了等待條件變量滿足的 wait 系列方法(wait、wait_for、wait_until 方法),發(fā)送條件信號使用?
notify 方法(notify_one 和 notify_all 方法),當(dāng)然使用 std::condition_variable 對象時需要綁定一個 std::unique_lock 或 std::lock_guard 對象,但是C++ 11 中 std::condition_variable 不
再需要顯式調(diào)用方法初始化和銷毀。