操作系統中同步、異步性概念
首先我們從操作系統的發展中學習什麼是異步性。在操作系統發展的初期階段,CPU處理的是作業,而且是單道批處理。什麼意思呢?就是一個作業從提交到結束,程序員都不能干預,此時整臺計算機就爲這一個作業服務(可想有多少資源被"浪費"),這樣有一點好處就是整個程序是"封閉的"。這樣的操作表明人和機器是沒有交互的。那我們怎麼實現人機交互呢?這個答案是中斷。中斷的引入,使得工作人員能在程序運行出問題的時候也能做出相應的處理。那麼在當前程序中斷後,計算機總不能讓CPU不做事吧,所以人們引入了新的概念——進程。當A進程不能繼續執行的時候(可能是因爲資源不足、競爭,或是等待I/O處理),A進程會阻塞,而B進程有足夠的資源,這時操作系統便把CPU分配給B進程。當然,這裏還涉及到了中斷處理程序。當A進程讓出CPU之前,中斷處理程序要做的是保護現場,即A進程的相關參數。當A進程等待的事件完成了,便可以返回中斷點重新開始工作。
簡單介紹發展史有助於我們更深刻的理解異步性的概念(當時我就是這樣一步一步把異步性、同步概念串起來的)。進程引入後,讓CPU的吞吐量得到了提升(若是單道批處理,作業等待I/O,那麼這個時候CPU也要等)。但帶來的問題是程序的運行失去了封閉性。異步性是指進程以不可預知的速度向前推進。在多道程序環境下,進程是併發執行的,不同進程之間存在着不同的相互制約關係(一般是資源問題)。內存中的每個進程何時執行,何時暫停,以怎樣的速度向前推進,程序總共需要多少時間才能完成等,都是不可預知的。例如,當正在執行的進程提出某種資源請求時,如打印機請求,而此時打印機正在爲其他的進程打印。由於打印機是臨界資源,因此正在執行的進程必須等待,並且要放棄處理機。直到打印機空閒,並再次把處理機分配給該進程時,該進程才能繼續執行。由於資源等因素的限制,進程的執行通常都不是 一氣呵成,而是以 停停走走 的方式運行。
試想以下兩個簡單的小程序是兩個進程,其中i是公共資源。
#include<stdio.h>//程序A
int i = 1;
int main()
{
i = i + 10;
//中間包含若干與i無關的操作
printf("Ai = %d", i);
return 0;
}
#include<stdio.h>//程序B
int i = 1;
int main()
{
i++;
//中間包含若干與i無關的操作
printf("Bi = %d", i);
return 0;
}
由於A和B是併發執行的,並且推進速度是不可預知的,所以最終的結果有多種情況。以下只分析兩種①:Ai = 11 Bi = 12 即:A先運行i = i + 10;並打印出來,B再運行i++。②:Ai = 12 Bi = 12 即:A先運行 i = i + 10此時並沒有打印,而B運行了i++後,A和B分別將i的值打印出來。其他情況也可以這樣分析。因爲異步性的關係,我們得到的答案可能是錯誤的,或是我們不想要的。爲了解決這個問題,就必須引入同步機制,使得程序能按照規則運行下去,從而得到我們想要的答案。
創建父子進程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid =fork();
if( pid == 0 ) //子進程返回值爲0
{
while(1)
{
printf("This is child\n");
sleep(1);
}
}
else
{
while(1)
{
printf("This is parent\n");
sleep(1);
}
}
return 0;
}
可以看到,父子進程之間打印的信息並沒有固定的先後順序。當父子進程同時去訪問資源時,也不能確定獲取資源的先後順序。這就表明進程的異步性可能出現我們不想要的結果,或者說是錯誤的結果。解決上述問題的方法就是"同步"。
同步亦稱直接制約關係,它是指爲完成某種任務而建立的兩個或多個進程,這些進程因爲需要在某些位置上協調它們的工作次序而等待、傳遞信息所產生的制約關係。進程間的直接制約關係就是源於它們之間的相互合作。例如:上面兩個程序可以看成5+3*5中的加法程序和乘法程序。若先執行乘法程序再執行加法程序,則5+3*5=20。這個答案一定是對的嗎?其實不然。如果我們想要的答案是40,就要先執行加法程序再執行乘法程序,(5+3)*5=40。我們通過加 () 改變了運算的先後順序,使先乘除後加減變成了先加減後乘除,這就是一種同步機制。
當我們理解了異步、同步的概念後,可以簡單瞭解一下互斥的概念。互斥亦稱間接制約關係。當一個進程進入臨界區使用臨界資源時,另一個進程必須等待, 當佔用臨界資源的進程退出臨界區後,另一進程才允許去訪問此臨界資源。例如,在僅有一臺打印機的系統中,有兩個進程A和進程B,如果進程A需要打印時,系統已將打印機分配給進程B,則進程A必須阻塞。一旦進程B將打印機釋放,系統便將進程A喚醒,並將其由阻塞狀態變爲就緒狀態。爲禁止兩個進程同時進入臨界區,同步機制應遵循以下準則:
空閒讓進:臨界區空閒時,可以允許一個請求進入臨界區的進程立即進入臨界區。
忙則等待:當已有進程進入臨界區時,其他試圖進入臨界區的進程必須等待。
有限等待:對請求訪問的進程,應保證能在有限時間內進入臨界區。
讓權等待:當進程不能進入臨界區時,應立即釋放處理器,防止進程忙等待。
線程同步例子(使用互斥鎖):
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
/*全局變量*/
int sum = 0;
/*互斥量 */
pthread_mutex_t mutex;
/*聲明線程運行服務程序*/
void* pthread_function1 (void*);
void* pthread_function2 (void*);
int main (void)
{
/*線程的標識符*/
pthread_t pt_1 = 0;
pthread_t pt_2 = 0;
int ret = 0;
/*互斥初始化*/
pthread_mutex_init (&mutex, NULL);
/*分別創建線程1、2*/
ret = pthread_create( &pt_1, //線程標識符指針
NULL, //默認屬性
pthread_function1, //運行函數
NULL); //無參數
if (ret != 0)
{
perror ("pthread_1_create");
}
ret = pthread_create( &pt_2, //線程標識符指針
NULL, //默認屬性
pthread_function2, //運行函數
NULL); //無參數
if (ret != 0)
{
perror ("pthread_2_create");
}
/*等待線程1、2的結束*/
pthread_join (pt_1, NULL);
pthread_join (pt_2, NULL);
printf ("main programme exit!\n");
return 0;
}
/*線程1的服務程序*/
void* pthread_function1 (void*a)
{
int i = 0;
printf ("This is pthread_1!\n");
for( i=0; i<3; i++ )
{
pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
/*臨界資源*/
sum++;
printf ("Thread_1 add one to num:%d\n",sum);
pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/
/*注意,這裏以防線程的搶佔,以造成一個線程在另一個線程sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖後調用*/
sleep (1);
}
pthread_exit ( NULL );
}
/*線程2的服務程序*/
void* pthread_function2 (void*a)
{
int i = 0;
printf ("This is pthread_2!\n");
for( i=0; i<5; i++ )
{
pthread_mutex_lock(&mutex); /*獲取互斥鎖*/
/*臨界資源*/
sum++;
printf ("Thread_2 add one to num:%d\n",sum);
pthread_mutex_unlock(&mutex); /*釋放互斥鎖*/
/*注意,這裏以防線程的搶佔,以造成一個線程在另一個線程sleep時多次訪問互斥資源,所以sleep要在得到互斥鎖後調用*/
sleep (1);
}
pthread_exit ( NULL );
}
Linux下編譯時需要加 -lpthread
注意第一個字母是大寫,windows C語言中單位是毫秒(ms)。
Sleep (500);
就是到這裏停半秒,然後繼續向下執行。
包含在#include <windows.h>頭文件
在Linux C語言中 sleep的單位是秒(s)
sleep(5);//停5秒
包含在 #include <unistd.h>頭文件
由於程序先創建的是 Thread_1,所以 Thread_1 先加鎖,即擁有使用公共資源的權限。Thread_1 在加鎖後休眠2秒,此時 Thread_2 被阻塞。若不加鎖,Thread_2 能直接對公共資源進行操作。當 Thread_1 的工作完成,它釋放互斥鎖資源,之後運行 Thread_2。同理,當 Thread_2 運行時,Thread_1 被阻塞,直至 Thread_2 完成工作並釋放互斥鎖資源。
經典進程同步問題:生產者-消費者問題
(1)描述:一組生產者進程和一組消費者進程共享一個初始爲空、大小爲 n 的緩衝區,只有緩衝區沒滿時,生產者才能把消息放入到緩衝區,否則必須等待;只有緩衝區不空時,消費者才能從中取出消息,否則必須等待。由於緩衝區是臨界資源,它只允許一個生產者放入消息,或者一個消費者從中取出消息。
(2)分析:
①關係分析。生產者和消費者對緩衝區互斥訪問是互斥關係,同時生產者和消費者又是一個相互協作的關係,只有生產者生產之後,消費者才能消費,他們也是同步關係。
②整理思路。這裏比較簡單,只有生產者和消費者兩個進程,正好是這兩個進程存在着互斥關係和同步關係。那麼需要解決的是互斥和同步 PV 操作的位置。
③信號量設置。信號量 mutex 作爲互斥信號量,它用於控制互斥訪問緩衝池,互斥信號量初值爲1;信號量 full 用於記錄當前緩衝池中
"滿"緩衝區數,初值爲0。信號量 empty 用於記錄當前緩衝池中"空"緩衝區數,初值爲 n。生產者-消費者進程的僞代碼如下:
semaphore mutex=1; //臨界區互斥信號量
semaphore empty=n; //空閒緩衝區
semaphore full=0; //緩衝區初始化爲空
producer () { //生產者進程
while(1){
produce an item in nextp; //生產數據
P(empty); //獲取空緩衝區單元
P(mutex); //進入臨界區.
add nextp to buffer; //將數據放入緩衝區
V(mutex); //離開臨界區,釋放互斥信號量
V(full); //滿緩衝區數加1
}
}
consumer () { //消費者進程
while(1){
P(full); //獲取滿緩衝區單元
P(mutex); // 進入臨界區
remove an item from buffer; //從緩衝區中取出數據
V (mutex); //離開臨界區,釋放互斥信號量
V (empty) ; //空緩衝區數加1
consume the item; //消費數據
}
}
該類問題要注意對緩衝區大小爲 n 的處理,當緩衝區中有空時便可對 empty 變量執行 P 操作,一旦取走一個產品便要執行 V 操作以釋放空閒區。對 empty 和 full 變量的 P 操作必須放在對 mutex 的P操作之前。如果生產者進程先執行 P(mutex),然後執行 P(empty),消費者執行 P(mutex),然後執行P(fall),這樣可不可以?答案是否定的。設想生產者進程已經將緩衝區放滿,消費者進程並沒有取產品,即empty = 0,當下次仍然是生產者進程運行時,它先執行 P(mutex) 封鎖信號量,再執行 P(empty) 時將被阻塞,希望消費者取出產品後將其喚醒。輪到消費者進程運行時,它先執行 P(mutex),然而由於生產者進程已經封鎖 mutex 信號量,消費者進程也會被阻塞,這樣一來生產者、消費者進程都將阻塞,都指望對方喚醒自己,陷入了無休止的等待。同理,如果消費者進程已經將緩衝區取空,即 full = 0,下次如果還是消費者先運行,也會出現類似的死鎖。不過生產者釋放信號量時,mutex、full 先釋放哪一個無所謂,消費者先釋放mutex 還是 empty 都可以。