執行緒同步----遞迴鎖
概述
最常見的程序/執行緒的同步方法有互斥鎖(或稱互斥量Mutex),讀寫鎖(rdlock),條件變數(cond),訊號量(Semophore)等。在Windows系統中,臨界區(Critical Section)和事件物件(Event)也是常用的同步方法。
簡單的說,互斥鎖保護了一個臨界區,在這個臨界區中,一次最多隻能進入一個執行緒。如果有多個程序在同一個臨界區內活動,就有可能產生競態條件(race condition)導致錯誤。
讀寫鎖從廣義的邏輯上講,也可以認為是一種共享版的互斥鎖。如果對一個臨界區大部分是讀操作而只有少量的寫操作,讀寫鎖在一定程度上能夠降低執行緒互斥產生的代價。
條件變數允許執行緒以一種無競爭的方式等待某個條件的發生。當該條件沒有發生時,執行緒會一直處於休眠狀態。當被其它執行緒通知條件已經發生時,執行緒才會被喚醒從而繼續向下執行。條件變數是比較底層的同步原語,直接使用的情況不多,往往用於實現高層之間的執行緒同步。使用條件變數的一個經典的例子就是執行緒池(Thread Pool)了。
在學習作業系統的程序同步原理時,講的最多的就是訊號量了。通過精心設計訊號量的PV操作,可以實現很複雜的程序同步情況(例如經典的哲學家就餐問題和理髮店問題)。而現實的程式設計中,卻極少有人使用訊號量。能用訊號量解決的問題似乎總能用其它更清晰更簡潔的設計手段去代替訊號量。
本系列文章的目的並不是為了講解這些同步方法應該如何使用(AUPE的書已經足夠清楚了)。更多的是講解很容易被人忽略的一些關於鎖的概念,以及比較經典的使用與設計方法。文章會涉及到遞迴鎖與非遞迴鎖(recursive mutex和non-recursive mutex),區域鎖(Scoped Lock),策略鎖(Strategized Locking),讀寫鎖與條件變數,雙重檢測鎖(DCL),鎖無關的資料結構(Locking free),自旋鎖等等內容,希望能夠拋磚引玉。
那麼我們就先從遞迴鎖與非遞迴鎖說開去吧:)
1 可遞迴鎖與非遞迴鎖
1.1 概念
在所有的執行緒同步方法中,恐怕互斥鎖(mutex)的出場率遠遠高於其它方法。互斥鎖的理解和基本使用方法都很容易,這裡不做更多介紹了。
Mutex可以分為遞迴鎖(recursive mutex)和非遞迴鎖(non-recursive mutex)。可遞迴鎖也可稱為可重入鎖(reentrant mutex),非遞迴鎖又叫不可重入鎖(non-reentrant mutex)。
二者唯一的區別是,同一個執行緒可以多次獲取同一個遞迴鎖,不會產生死鎖。而如果一個執行緒多次獲取同一個非遞迴鎖,則會產生死鎖。
Windows下的Mutex和Critical Section是可遞迴的。Linux下的pthread_mutex_t鎖預設是非遞迴的。可以顯示的設定PTHREAD_MUTEX_RECURSIVE屬性,將pthread_mutex_t設為遞迴鎖。
在大部分介紹如何使用互斥量的文章和書中,這兩個概念常常被忽略或者輕描淡寫,造成很多人壓根就不知道這個概念。但是如果將這兩種鎖誤用,很可能會造成程式的死鎖。請看下面的程式。
MutexLock mutex;
void foo()
{
mutex.lock();
// do something
mutex.unlock();
}
void bar()
{
mutex.lock();
// do something
foo();
mutex.unlock();
}
foo函式和bar函式都獲取了同一個鎖,而bar函式又會呼叫foo函式。如果MutexLock鎖是個非遞迴鎖,則這個程式會立即死鎖。因此在為一段程式加鎖時要格外小心,否則很容易因為這種呼叫關係而造成死鎖。
不要存在僥倖心理,覺得這種情況是很少出現的。當代碼複雜到一定程度,被多個人維護,呼叫關係錯綜複雜時,程式中很容易犯這樣的錯誤。慶幸的是,這種原因造成的死鎖很容易被排除。
但是這並不意味著應該用遞迴鎖去代替非遞迴鎖。遞迴鎖用起來固然簡單,但往往會隱藏某些程式碼問題。比如呼叫函式和被呼叫函式以為自己拿到了鎖,都在修改同一個物件,這時就很容易出現問題。因此在能使用非遞迴鎖的情況下,應該儘量使用非遞迴鎖,因為死鎖相對來說,更容易通過除錯發現。程式設計如果有問題,應該暴露的越早越好。
1.2 如何避免
為了避免上述情況造成的死鎖,AUPE v2一書在第12章提出了一種設計方法。即如果一個函式既有可能在已加鎖的情況下使用,也有可能在未加鎖的情況下使用,往往將這個函式拆成兩個版本---加鎖版本和不加鎖版本(新增nolock字尾)。
例如將foo()函式拆成兩個函式。
// 不加鎖版本
void foo_nolock()
{
// do something
}
// 加鎖版本
void foo()
{
mutex.lock();
foo_nolock();
mutex.unlock();
}
遞迴鎖的例項,在同一個執行緒中的遞迴鎖
//執行緒屬性
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t g_mutex;
void test_fun(void);
static void thread_init(void)
{
//初始化鎖的屬性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE_NP);//設定鎖的屬性為可遞迴
//設定鎖的屬性
pthread_mutex_init(&g_mutex, &attr);
//銷燬
pthread_mutexattr_destroy(&attr);
}
//執行緒執行函式
void* thr_fun(void* arg)
{
int ret;
ret=pthread_mutex_lock(&g_mutex);
if( ret!=0 )
{
perror("thread pthread_mutex_lock");
exit(1);
}
printf("this is a thread !/n");
test_fun();
ret=pthread_mutex_unlock(&g_mutex);
if( ret!=0 )
{
perror("thread pthread_mutex_unlock");
exit(1);
}
return NULL;
}
//測試函式
void test_fun(void)
{
int ret;
ret=pthread_mutex_lock(&g_mutex);
if( ret!=0 )
{
perror("test pthread_mutex_lock");
exit(1);
}
printf("this is a test!/n");
ret=pthread_mutex_unlock(&g_mutex);
if( ret!=0 )
{
perror("test pthread_mutex_unlock");
exit(1);
}
}
int main(int argc, char *argv[])
{
int ret;
thread_init();
pthread_t tid;
ret=pthread_create(&tid, NULL, thr_fun, NULL);
if( ret!=0 )
{
perror("thread create");
exit(1);
}
pthread_join(tid, NULL);
return 0;
}
執行結果為:
this is a thread !
this is a test!
詳細說明:
型別互斥量屬性控制著互斥量的特性。POSIX定義了四種類型。
enum
{
PTHREAD_MUTEX_TIMED_NP,
PTHREAD_MUTEX_RECURSIVE_NP,
PTHREAD_MUTEX_ERRORCHECK_NP,
PTHREAD_MUTEX_ADAPTIVE_NP
};
其中,PTHREAD_MUTEX_TIMED_NP型別是標準(預設)的互斥量型別,並不作任何特殊的錯誤檢查或死鎖檢查。PTHREAD_MUTEX_RECURSIVE_NP互斥量型別允許同一執行緒在互斥量解鎖之前對該互斥量進行多次加鎖。同一個遞迴互斥量維護鎖的計數,在解鎖的次數和加鎖次數不同的情況下不會釋放鎖。即對同一互斥量加幾次鎖就要解幾次鎖。
涉及的函式
1.互斥量屬性的初始化與回收
#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
返回值:若成功返回0,否則返回錯誤編號。
2.獲取/設定互斥量屬性
#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,
int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
返回值:若成功返回0,否則返回錯誤編號。
測試程式:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
pthread_mutex_t lock;
int g_val0, g_val1;
int func(void)
{
int ret, val;
ret = pthread_mutex_lock(&lock);
if (ret)
printf("func:lock:%s\n", strerror(ret));
val = g_val1+8;
#if 1
ret = pthread_mutex_unlock(&lock);
if (ret)
printf("func:unlock%s\n", strerror(ret));
#endif
return val;
}
void * test0(void * arg)
{
int ret;
ret = pthread_mutex_lock(&lock);
if (ret)
printf("lock:%s\n", strerror(ret));
sleep(5);
g_val0 = func();
printf("res=%d\n", g_val0);
ret = pthread_mutex_unlock(&lock);
if (ret)
printf("unlock%s\n", strerror(ret));
return NULL;
}
void * test1(void * arg)
{
sleep(1);
#if 1
int ret = pthread_mutex_lock(&lock);
if (ret)
printf("1:%s\n", strerror(ret));
printf("g_val0=%d\n", g_val0);
ret = pthread_mutex_unlock(&lock);
if (ret)
printf("1:unlock%s\n", strerror(ret));
#endif
return NULL;
}
int main(void)
{
int ret;
pthread_t tid[2];
pthread_attr_t attr;
pthread_mutexattr_t mutexattr;
pthread_attr_init(&attr);
pthread_mutexattr_init(&mutexattr);
pthread_attr_setdetachstate(&attr,
PTHREAD_CREATE_DETACHED);
pthread_mutexattr_settype(&mutexattr,
PTHREAD_MUTEX_RECURSIVE_NP);
pthread_mutex_init(&lock, &mutexattr);
pthread_mutexattr_destroy(&mutexattr);
ret = pthread_create(&tid[0], &attr,
test0, NULL);
if (ret) {
fprintf(stderr, "create:%s\n", strerror(ret));
exit(1);
}
ret = pthread_create(&tid[0], &attr,
test1, NULL);
if (ret) {
fprintf(stderr, "create:%s\n", strerror(ret));
exit(1);
}
pthread_attr_destroy(&attr);
pthread_exit(NULL);
}