您當前的位置:首頁 > 遊戲

POSIX執行緒2——訊號量和互斥量

作者:由 松偉 發表于 遊戲時間:2022-09-20

之前我們說過

臨界區

的概念。有兩種基本的方法可以控制程式碼對臨界區的訪問:訊號量和互斥量。我們一個一個來說。

訊號量

學過作業系統理論的人應該對這個概念不陌生。它是由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

);

}

POSIX執行緒2——訊號量和互斥量

從截圖中來看,程式沒有問題。。。嗎?

俗話說的好,當你想用多執行緒解決一個問題時,你將面臨兩個問題:原本的問題,和因為多執行緒引起的新問題。我們稍微改改程式碼,只修改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

);

}

POSIX執行緒2——訊號量和互斥量

輸入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

);

}

POSIX執行緒2——訊號量和互斥量

這次實驗用的方式是

輪詢

互斥量,實際的程式設計中,輪詢的效率並不高。按照書上的說法,我們還是應該儘可能使用訊號量。

標簽: mutex  訊號量  SEM  work  pthread