POSIX執行緒2——訊號量和互斥量
之前我們說過
臨界區
的概念。有兩種基本的方法可以控制程式碼對臨界區的訪問:訊號量和互斥量。我們一個一個來說。
訊號量
學過作業系統理論的人應該對這個概念不陌生。它是由Dijkstra首次提出(對,就是那個弄出最短路徑演算法的傢伙),它是一種特殊的變數,可以增加或者減少,其特殊的地方在於該變數無論是增加還是減少都是
原子操作
(即絕對不會被中斷)。在多執行緒中,當兩個以上的執行緒試圖改變同一個訊號量時,系統將保證所有的操作都是
依次進行
,而不是像一般變數那樣,結果無法預計。
訊號量的實際理解
作業系統學的特別好,對訊號量非常熟悉的可以跳過這一部分。
訊號量我覺得,就是:資源的計數器。比如訊號量等於5,表示這個資源有5份。
我們舉一個現實生活中的例子:你去飯店吃飯,卻因為人多需要等位子。
對於顧客來說,最重要的資源是什麼?飯店中的座位。假設飯店中有10個座位,那麼其座位計數器初始化為10。當有一個顧客需要用餐時,他會先問門口接待:“有沒有座位?”接待查詢計數器,如果還有座位,則會將計數器減一併讓顧客佔用一個座位用餐;如果計數器為0則會告訴顧客:“沒有座位了。”顧客可以選擇在門口等座位或者轉身離開去做別的事。
當顧客用餐結束後,座位會空出來,則門口接待的座位計數器加一,表示空出了一個座位。空出來的座位可以被其他顧客使用。
在以上的例子中,座位就是資源,計數器就是訊號量,顧客就是想要佔用資源的程序/執行緒。想要佔用資源的行為被稱為P操作,完成任務釋放資源的行為被稱為V操作(合起來就是作業系統理論中的PV操作)。至於沒有座位時顧客是在門口等還是轉身離開,取決於顧客P操作的方法是阻塞的還是非阻塞的。
PS:現在餐館好像會給排隊的人發一個序號吧,哪怕你離開去做別的事,回來後還可以依靠序號而不用重新排隊。可惜Linux作業系統沒有那麼“人性化”的功能。順便,現實中只有十個座位的餐館怎麼可能還有門口接待!
訊號量的操作
訊號量函式的名字都以sem_開頭。執行緒中使用的基本訊號量函式有4個。
#include
int
sem_init
(
sem_t
*
sem
,
int
pshared
,
unsigned
int
value
);
sem_init用於初始化一個訊號量,初始值為value。pshared引數控制訊號量的型別,如果值為0,則表示這個訊號量是當前程序的區域性訊號量,否則,這個訊號量可以在多程序之間共享。我們現在討論的是執行緒之間的同步,所以只對不能在程序間共享的訊號量感興趣,所以使用時給pshared傳入0即可。
#include
int
sem_wait
(
sem_t
*
sem
);
int
sem_post
(
sem_t
*
sem
);
以上兩個函式用於實現之前說的PV操作。sem_wait操作就是P操作,以原子操作將訊號量減一,如果訊號量為0則阻塞等待直到其非0再減一。sem_post操作就是V操作,以原子操作將訊號量加一。
sem_wait的非阻塞版本是sem_trywait,這裡不多討論
。
#include
int
sem_destroy
(
sem_t
*
sem
);
清理/釋放/銷燬一個訊號量。如果sem正在被一些執行緒等待,就會收到一個錯誤。
小實驗
主執行緒用於接受輸入的字串,新執行緒用於統計字串的字元數。這裡我們設定二進位制訊號量(即訊號量的值只有0或1)。設定訊號量的同時,我們等待著鍵盤輸入。當輸入到達時,我們釋放訊號量,允許第二個執行緒統計輸入的字元數。(可以理解成:輸入的字串就是資源。因為只有當有字串輸入,第二個執行緒才能統計字元數。)
#include
#include
#include
#include
#include
#include
void
*
thread_function
(
void
*
arg
);
sem_t
bin_sem
;
//定義為全域性,才能執行緒間共享
#define WORK_SIZE 1024
char
work_area
[
WORK_SIZE
];
int
main
()
{
int
res
;
pthread_t
a_thread
;
void
*
thread_result
;
//首先初始化訊號量,初始化為0(因為此時沒有字串輸入)
res
=
sem_init
(
&
bin_sem
,
0
,
0
);
if
(
res
!=
0
)
{
perror
(
“semaphore init failed
\n
”
);
exit
(
EXIT_FAILURE
);
}
//建立新執行緒
res
=
pthread_create
(
&
a_thread
,
NULL
,
thread_function
,
NULL
);
if
(
res
!=
0
)
{
perror
(
“thread create failed
\n
”
);
exit
(
EXIT_FAILURE
);
}
printf
(
“input your message。 if you want to quit, enter ‘end’
\n
”
);
//輸入字串
while
(
strncmp
(
“end”
,
work_area
,
3
)
!=
0
)
{
fgets
(
work_area
,
WORK_SIZE
,
stdin
);
sem_post
(
&
bin_sem
);
//給訊號量加一
}
//等待新執行緒結束
printf
(
“
\n
waiting for thread to finish。。
\n
”
);
res
=
pthread_join
(
a_thread
,
&
thread_result
);
if
(
res
!=
0
)
{
perror
(
“thread join failed
\n
”
);
exit
(
EXIT_FAILURE
);
}
//收尾
printf
(
“thread joined
\n
”
);
sem_destroy
(
&
bin_sem
);
exit
(
EXIT_SUCCESS
);
}
void
*
thread_function
(
void
*
arg
)
{
sem_wait
(
&
bin_sem
);
while
(
strncmp
(
“end”
,
work_area
,
3
)
!=
0
)
{
printf
(
“the num of char is %d
\n
”
,
strlen
(
work_area
)
-
1
);
sem_wait
(
&
bin_sem
);
}
pthread_exit
(
NULL
);
}
從截圖中來看,程式沒有問題。。。嗎?
俗話說的好,當你想用多執行緒解決一個問題時,你將面臨兩個問題:原本的問題,和因為多執行緒引起的新問題。我們稍微改改程式碼,只修改main函式中的while迴圈。注意,這個新的程式碼是錯誤的。我們附上main函式中的while修改後的程式碼,其他部分和原來相同。
while
(
strncmp
(
“end”
,
work_area
,
3
)
!=
0
)
{
if
(
strncmp
(
work_area
,
“FAST”
,
4
)
==
0
)
{
sem_post
(
&
bin_sem
);
strcpy
(
work_area
,
“hello world”
);
}
else
{
fgets
(
work_area
,
WORK_SIZE
,
stdin
);
}
sem_post
(
&
bin_sem
);
}
輸入FAST,程式就會先增加訊號量讓新執行緒開始執行,同時立刻用hello world更新陣列。從圖中可以看出,計數統計是錯誤的。
問題就出在我們是先增加訊號量再去修改陣列。我們的程式統計輸入字串的時間要足夠長,這樣另一個執行緒才有時間在主程序給其新的字串之前統計出結果。當我們快速切換字串(鍵盤輸入FAST和程式賦予的hello world)時,第二個執行緒就沒有時間去執行,但是訊號量又增加了好幾次,所以新執行緒就會反覆統計資料直到訊號量用完。
互斥量
互斥量類似於
鎖
,它允許程式設計師鎖住某個資源,從而讓每次只有一個執行緒去訪問它。我們可以在進入臨界區時對互斥量上鎖,然後在完成操作後解鎖。 Linux提供了一系列方法去操作互斥量,其操作方法和訊號量非常像,也是主要有四個方法。
#include
int
pthread_mutex_init
(
pthread_mutex_t
*
mutex
,
const
pthread_mutexattr_t
*
mutexattr
);
int
pthread_mutex_lock
(
pthread_mutex_t
*
mutex
);
int
pthread_mutex_unlock
(
pthread_mutex_t
*
mutex
);
int
pthread_mutex_destroy
(
pthread_mutex_t
*
mutex
);
從字面意思我們也可以才出來每個函式的意思。和其他函式一樣,成功返回0,失敗返回
錯誤程式碼
(注意不是-1)。這些函式並不設定errno,所以我們必須對其返回值進行檢查。
pthread_mutex_init函式的第二個引數可以設定互斥量的屬性。如果傳遞NULL,則採用預設屬性fast。採用該屬性時,如果程式試圖lock一個已經加鎖的互斥量,程式會被阻塞直到互斥量原本的鎖解開。但是有一個
小缺點
:如果同一個執行緒重複給同一個互斥量加鎖,第一次加鎖可以成功,但是第二次加鎖會被阻塞以等待解鎖。可是擁有解鎖這個互斥量能力的執行緒就是當前被阻塞的執行緒,互斥量永遠不會解鎖,執行緒永遠被阻塞。也就是之前說的死鎖。這個問題可以透過改變互斥量的屬性來解決,可是我不打算在這裡討論互斥量的其他屬性。總之。。。。用的時候注意點唄,別自己給自己挖坑。
現在我們再來看看訊號量那裡用的例子:主執行緒輸入字串,新執行緒統計字數。只是這次我們用互斥量來實現。我們出於簡化程式的考量,暫時不管mutex相關函式的返回值。(當然實際程式設計中,對返回值的檢查是必不可少的)。在這個程式中,互斥量主要鎖的是工作區,所以接下來我在註釋中可能會直接將對互斥量上鎖說成:“對工作區上鎖”以方便理解。
以下是程式原始碼:
#include
#include
#include
#include
#include
#include
void
*
thread_function
(
void
*
arg
);
pthread_mutex_t
work_mutex
;
//互斥量定義,主要鎖的是工作區
#define WORK_SIZE 1024
char
work_area
[
WORK_SIZE
];
int
time_to_exit
=
0
;
int
main
()
{
int
res
;
pthread_t
a_thread
;
void
*
thread_result
;
res
=
pthread_mutex_init
(
&
work_mutex
,
NULL
);
if
(
res
!=
0
)
{
perror
(
“mutex init failed
\n
”
);
exit
(
EXIT_FAILURE
);
}
res
=
pthread_create
(
&
a_thread
,
NULL
,
thread_function
,
NULL
);
if
(
res
!=
0
)
{
perror
(
“thread create failed
\n
”
);
exit
(
EXIT_FAILURE
);
}
pthread_mutex_lock
(
&
work_mutex
);
//對互斥量上鎖
printf
(
“input your message。 if you want to quit, enter ‘end’
\n
”
);
while
(
!
time_to_exit
)
{
fgets
(
work_area
,
WORK_SIZE
,
stdin
);
pthread_mutex_unlock
(
&
work_mutex
);
//讀入文字後解鎖方便新執行緒使用
//以下迴圈為週期性嘗試加鎖,檢查字元數目是否已經統計完
while
(
1
)
{
pthread_mutex_lock
(
&
work_mutex
);
if
(
work_area
[
0
]
!=
‘\0’
)
{
//表示字元數已經統計完,新執行緒已經釋放工作區
pthread_mutex_unlock
(
&
work_mutex
);
sleep
(
1
);
}
else
{
break
;
}
}
}
pthread_mutex_unlock
(
&
work_mutex
);
printf
(
“
\n
waiting for thread to finish。。
\n
”
);
res
=
pthread_join
(
a_thread
,
&
thread_result
);
if
(
res
!=
0
)
{
perror
(
“thread join failed
\n
”
);
exit
(
EXIT_FAILURE
);
}
//收尾
printf
(
“thread joined
\n
”
);
pthread_mutex_destroy
(
&
work_mutex
);
exit
(
EXIT_SUCCESS
);
}
void
*
thread_function
(
void
*
arg
)
{
sleep
(
1
);
pthread_mutex_lock
(
&
work_mutex
);
//對工作區上鎖
while
(
strncmp
(
“end”
,
work_area
,
3
)
!=
0
)
{
//判斷是不是要結束程式
printf
(
“the num of char is %d
\n
”
,
strlen
(
work_area
)
-
1
);
work_area
[
0
]
=
‘\0’
;
//下面的內容為:每隔一秒嘗試給互斥量加鎖,如果加鎖成功,就檢查是否有新字串要處理
//如果沒有,則解鎖互斥量並接著等待
pthread_mutex_unlock
(
&
work_mutex
);
sleep
(
1
);
pthread_mutex_lock
(
&
work_mutex
);
while
(
work_area
[
0
]
==
‘\0’
)
{
pthread_mutex_unlock
(
&
work_mutex
);
sleep
(
1
);
pthread_mutex_lock
(
&
work_mutex
);
}
}
//執行緒結束的收尾工作
time_to_exit
=
1
;
//跳出迴圈表示程式要退出了
work_area
[
0
]
=
‘\0’
;
pthread_mutex_unlock
(
&
work_mutex
);
pthread_exit
(
0
);
}
這次實驗用的方式是
輪詢
互斥量,實際的程式設計中,輪詢的效率並不高。按照書上的說法,我們還是應該儘可能使用訊號量。