概述
最常見的進程/線程的同步方法有互斥鎖(或稱互斥量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設為遞歸鎖。
在大部分介紹如何使用互斥量的文章和書中,這兩個概念常常被忽略或者輕描淡寫,造成很多人壓根就不知道這個概念。但是如果將這兩種鎖誤用,很可能會造成程序的死鎖。請看下面的程序。
[cpp]
SPAN style="FONT-SIZE: 18px"><STRONG>MutexLock mutex;
void foo()
{
mutex.lock();
// do something
mutex.unlock();
}
void bar()
{
mutex.lock();
// do something
foo();
mutex.unlock();
} </STRONG></SPAN>
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()函數拆成兩個函數。
[cpp]
<SPAN style="FONT-SIZE: 18px"><STRONG>// 不加鎖版本
void foo_nolock()
{
// do something
}
// 加鎖版本
void foo()
{
mutex.lock();
foo_nolock();
mutex.unlock();
} </STRONG></SPAN>
// 不加鎖版本
void foo_nolock()
{
// do something
}
// 加鎖版本
void foo()
{
mutex.lock();
foo_nolock();
mutex.unlock();
}
遞歸鎖的實例,在同一個線程中的遞歸鎖
[cpp]
//線程屬性
#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;
}
//線程屬性
#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,否則返回錯誤編號。
測試程序:
[cpp]
#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);
}
#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);
}