環境:
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的信息,或者反之。
但觀察前三種,基本上的效果是一個線程輸出一下,交織頻繁。
總結:
前三種方式,依賴一個句柄,
他們都可以指定一個名字,成爲全局的對象,
可以完成進程間的同步。
在不用的時候要,銷燬相關的句柄。
消耗資源比較大。
最後一種臨界區,消耗資源非常少,速度快。
但是隻能解決線程間的同步。
幾種同步手段(互斥量,信號量,事件,臨界區)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.