操作系統中的同步和異步

操作系統中同步、異步性概念

首先我們從操作系統的發展中學習什麼是異步性。在操作系統發展的初期階段,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 都可以。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章