幾種同步手段(互斥量,信號量,事件,臨界區)

環境:

VISTA+VC6
雙核

這個環境對於下面的有些效果來說,十分關鍵。
在我下面的練習中,如果是單核,那麼兩個線程無法真正的同時執行,
而單個操作的耗時也並不長,可能看不到互斥訪問中的一些問題。
在VISTA之前的一些系統,時間片比較大,也不容易看到。。

設計目標:


模擬一個售票系統,有兩個線程可以出售,總共100張票。
中間打印出出售的信息。
這裏的票是一個臨界資源,
同時,控制檯也是個臨界資源。(如果同時輸出會造成屏幕的混亂)

原始程序:

#include <stdio.h>
#include <iostream>
#include <windows.h>

using namespace std;

int total=100;

DWORD WINAPI proc1(
LPVOID lpParameter   // thread data
){
    while (1){
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread1 sold:"<<total--<<endl;
    }
    return 0;
};

DWORD WINAPI proc2(
LPVOID lpParameter   // thread data
){
    while (1){
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread2 sold:"<<total--<<endl;
    }
    return 0;
};

int main(){
    HANDLE thread1,thread2;
    thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
    thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
    Sleep(4000);
    CloseHandle(thread1);
    CloseHandle(thread2);
    return 0;
}

程序的意思很直觀,就是開了兩個線程。
在裏面分別判斷票數是否到0,
如果不是的話,那麼模擬售出了一張票,並且打印出售出的票號。

中間標紅的隨機延遲是一個關鍵點。
把他去掉的話,一般就看不到效果了。
因爲電腦實在太快了,if的判斷和下面的輸出,
幾乎是在同一時間完成的。
從時間片的意義上來說,大部分時候可以看做原子操作。
於是減到0之後,線程正常結束就停下了。
所以給個隨機延遲,強迫if的判斷和total--的分離,
這樣就可以看到由於沒有做好同步造成的問題了。

這個程序的輸出,有的地方會有字符交叉,很混亂。
最明顯的是,減到0之後,還會不斷地向下面減。

同步的框架:

下面幾個方法,大同小異,
基本上的過程就是:

1.定義相關的變量
2.創建相關的變量
3.進去臨界區前等待相關的信號
4.退出的時候清除相關的信號
(信號有的時候可以進入臨界區,還是信號無的時候可以進入,
在幾個實現手段裏面有不同的敘述,所以清除是個泛化的說法)

互斥量:

#include <stdio.h>
#include <iostream>
#include <windows.h>

using namespace std;

int total=100;
HANDLE mutex;

DWORD WINAPI proc1(
LPVOID lpParameter   // thread data
){
    while (1){
       WaitForSingleObject(mutex,INFINITE);
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread1 sold:"<<total--<<endl;
        ReleaseMutex(mutex);
    }
    return 0;
};

DWORD WINAPI proc2(
LPVOID lpParameter   // thread data
){
    while (1){
        WaitForSingleObject(mutex,INFINITE);
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread2 sold:"<<total--<<endl;
        ReleaseMutex(mutex);
    }
    return 0;
};

int main(){
    HANDLE thread1,thread2;
    mutex=CreateMutex(NULL,false,NULL);
    thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
    thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
    Sleep(4000);
    CloseHandle(thread1);
    CloseHandle(thread2);
    CloseHandle(mutex);
    return 0;
}

這是最基本的,和框架非常吻合,
知道標紅的幾個函數就按照這種方式寫就行了。

信號量:

#include <stdio.h>
#include <iostream>
#include <windows.h>

using namespace std;

int total=100;

HANDLE semaphore ;


DWORD WINAPI proc1(
LPVOID lpParameter   // thread data
){
    while (1){
        WaitForSingleObject(semaphore,INFINITE);
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread1 sold:"<<total--<<endl;
        ReleaseSemaphore(semaphore , 1 , NULL) ;
    }
    return 0;
};

DWORD WINAPI proc2(
LPVOID lpParameter   // thread data
){
    while (total>0){
        WaitForSingleObject(semaphore,INFINITE);
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread2 sold:"<<total--<<endl;
        ReleaseSemaphore(semaphore , 1 , NULL) ;
    }
    return 0;
};

int main(){
    HANDLE thread1,thread2;
    semaphore = CreateSemaphore(NULL , 1 , 1 , NULL) ;

    thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
    thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);

    Sleep(4000);
    CloseHandle(thread1);
    CloseHandle(thread2);
    CloseHandle(semaphore) ;

    return 0;
}

和互斥量不同的地方在於,信號量可以允許多個線程同時訪問。
比如writer/reader模型中,多個reader同時訪問是允許的。
在創建的時候,可以指定最大的數目和初始化時候的數目。
如果指定爲1,也就是這裏用的情況,相當於就和前面的互斥量方式一樣了。

事件:

#include <stdio.h>
#include <iostream>
#include <windows.h>

using namespace std;

int total=100;

HANDLE event;

DWORD WINAPI proc1(
LPVOID lpParameter   // thread data
){
    while (1){
        WaitForSingleObject(event,INFINITE);
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread1 sold:"<<total--<<endl;
        SetEvent(event) ;
    }
    return 0;
};

DWORD WINAPI proc2(
LPVOID lpParameter   // thread data
){
    while (total>0){
        WaitForSingleObject(event,INFINITE);
        if ( total == 0 ) break ;
        Sleep(rand()%30) ;
        cout<<"thread2 sold:"<<total--<<endl;
        SetEvent(event) ;
    }
    return 0;
};

int main(){
    HANDLE thread1,thread2;
    event = CreateEvent(NULL , FALSE , TRUE , NULL) ;
    thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
    thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
    Sleep(4000);
    CloseHandle(thread1);
    CloseHandle(thread2);
    CloseHandle(event);
    return 0;
}

CreateEvent的第二個參數是設置是否爲手動事件。
如果是手動的話,當用WaitForSingleObject等到事件的時候,
系統並不清除掉該事件已發生的信號,
於是要自己調用ResetEvent來清除。
這兩個函數之間的空隙將造成潛在的同步問題。
於是設置生FALSE,表示自動事件。
在等到該事件的時候,同時把該事件置爲無效,防止其他地方進入臨界段。

臨界區:

#include <stdio.h>
#include <iostream>
#include <windows.h>

using namespace std;

int total=100;

CRITICAL_SECTION _cs;

DWORD WINAPI proc1(
LPVOID lpParameter   // thread data
){
    while (1){
       EnterCriticalSection(&_cs);
        if ( total == 0 ) break ;
    //    Sleep(rand()%30) ;
        cout<<"thread1 sold:"<<total--<<endl;
        LeaveCriticalSection(&_cs);
    }
    return 0;
};

DWORD WINAPI proc2(
LPVOID lpParameter   // thread data
){
    while (1){
        EnterCriticalSection(&_cs);
        if ( total == 0 ) break ;
    //    Sleep(rand()%30) ;
        cout<<"thread2 sold:"<<total--<<endl;
        LeaveCriticalSection(&_cs);
    }
    return 0;
};

int main(){
    HANDLE thread1,thread2;
   InitializeCriticalSection(&_cs);
    thread1=CreateThread(NULL,0,proc1,NULL,0,NULL);
    thread2=CreateThread(NULL,0,proc2,NULL,0,NULL);
    Sleep(4000);
    CloseHandle(thread1);
    CloseHandle(thread2);
    return 0;
}

與前面的相比,這種方式在最後不用類似CloseHandle之類的操作。

還有注意到我把上面的Sleep註釋掉了。
因爲使用臨界區來同步,速度非常快,消耗資源比前幾種都小
加上隨機延遲後,可能一個線程直接就把票給售完了。。

即使在現在這種寫法下,可能運行好幾次,
能夠找到一下若干thread1信息之內夾雜幾個thread2的信息,或者反之。

但觀察前三種,基本上的效果是一個線程輸出一下,交織頻繁。

總結:

前三種方式,依賴一個句柄,
他們都可以指定一個名字,成爲全局的對象,
可以完成進程間的同步。
在不用的時候要,銷燬相關的句柄。
消耗資源比較大。

最後一種臨界區,消耗資源非常少,速度快。
但是隻能解決線程間的同步。

發佈了5 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章