現代操作系統都支持多線程操作了,多線程操作帶來的一個麻煩就是多個線程對共享數據的訪問。假設我們有線程A
和線程B,它們需要訪問同一內存區域,線程A寫,線程B讀。一般情況下我們是希望線程A寫操作完成後再進行讀操
作或者線程B讀操作完成後我們再進行寫操作。但是在多線程中,可能由於線程A分配的時間片用完了或者其他原因導
致線程A的寫操作還沒完成就調用線程B來對這塊共享內存進行讀操作,也有可能在線程B的讀操作還沒完成就調用線
程A來對這塊共享內存進行寫操作,這些情況都有可能導致嚴重的邏輯錯誤。為了解決這一現象,我們就需要一種機
制使得各個線程能夠協同工作,這就是我們所講的線程同步機制了。
Windows系統中用於線程同步的常用機制有:
互斥對象(Mutex)
事件對象(Event)
信號量(Semaphore)
臨界區(critical section)
可等待計時器(Waitable Timer)
在學習線程同步之前,我們需要先來了解下同步過程中最重要的兩個概念:同步對象和等待函數。
同步對象主要有(Mutex、Event、Semaphore、critical section)。同步對象一般具有兩種狀態:標志的和未標志的。線
程根據是否已經完成操作將同步對象設置為標志的或未標志的。
而等待函數的功能是專門用於等待同步對象狀態改變。一個線程調用等待函數後執行會暫停,直到同步對象的狀態變
為標志的之後,等待函數才會返回,線程才能繼續執行下去。
關於上面所講的幾種主要同步對象的概念,這篇臨界區,互斥量,信號量,事件的區別講的很詳細,不懂的朋友可以
去肯看。
線程同步的過程:
1、在需要進行線程同步的進程中定義某種同步對象,同步對象必需是全局的,以保證需要同步的所有線程都可以訪
問到同步對象。
2、開始時,所有的線程相互獨立地運行
3、當某一線程(為了方便描述設為線程A)需要訪問共享資源時,若同步對象為“未標志的”,繼續等待;反之,線程A將
同步對象設為“未標志的”,並對共享資源進行訪問,訪問結束後再將同步對象設為“標志的”使得其它線程可以訪問
共享資源。
為了便於理解線程同步的過程,我們可以把我們需要訪問的共享資源當成是一件放在房間裡的東西,而同步對象當成
是門上的鎖,而需要訪問資源的線程就可以當做是取東西的人了,“標志的”狀態表示門是開的,“未標志的”狀態表示
門是鎖著的,而此時鑰匙在進去的那個人手裡。當某人進入房間後,就將門鎖上,其他人就無法進入了,只有等這個
人出來之後才能進入。
下面是一個我自己寫的利用事件對象來同步訪問共享內存實例:
[cpp]
#include <windows.h>
#include <stdio.h>
#include <string.h>
TCHAR szSharedBuffer[100] = {0}; //共享內存
HANDLE hEvent; //事件對象句柄
DWORD WINAPI ThreadForWrite (LPVOID lpParam);
DWORD WINAPI ThreadForRead (LPVOID lpParam);
int main()
{
HANDLE hWrite;
HANDLE hRead;
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hWrite = CreateThread(NULL,
0,
ThreadForWrite,
0,
0,
NULL);
hRead = CreateThread(NULL,
0,
ThreadForRead,
0,
0,
NULL);
SetEvent(hEvent);
while(1);
return 0;
}
DWORD WINAPI ThreadForWrite(LPVOID lpParam)
{
while (1)
{
WaitForSingleObject(hEvent, INFINITE);
printf("Please input the shared chars: ");
scanf("%s", szSharedBuffer);
SetEvent(hEvent);
}
return 0;
}
DWORD WINAPI ThreadForRead(LPVOID lpParam)
{
while (1)
{
WaitForSingleObject(hEvent, INFINITE);
if (!strlen(szSharedBuffer))
printf("The shared chars is null now!\n");
else
printf("The shared chars is %s\n", szSharedBuffer);
SetEvent(hEvent);
}
return 0;
}
#include <windows.h>
#include <stdio.h>
#include <string.h>
TCHAR szSharedBuffer[100] = {0}; //共享內存
HANDLE hEvent; //事件對象句柄
DWORD WINAPI ThreadForWrite (LPVOID lpParam);
DWORD WINAPI ThreadForRead (LPVOID lpParam);
int main()
{
HANDLE hWrite;
HANDLE hRead;
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
hWrite = CreateThread(NULL,
0,
ThreadForWrite,
0,
0,
NULL);
hRead = CreateThread(NULL,
0,
ThreadForRead,
0,
0,
NULL);
SetEvent(hEvent);
while(1);
return 0;
}
DWORD WINAPI ThreadForWrite(LPVOID lpParam)
{
while (1)
{
WaitForSingleObject(hEvent, INFINITE);
printf("Please input the shared chars: ");
scanf("%s", szSharedBuffer);
SetEvent(hEvent);
}
return 0;
}
DWORD WINAPI ThreadForRead(LPVOID lpParam)
{
while (1)
{
WaitForSingleObject(hEvent, INFINITE);
if (!strlen(szSharedBuffer))
printf("The shared chars is null now!\n");
else
printf("The shared chars is %s\n", szSharedBuffer);
SetEvent(hEvent);
}
return 0;
}
當然這裡只是對線程同步進行一個簡單的說明,真正要掌握線程同步比這裡所寫的要復雜的多。這將在以後的學習中
慢慢補充。
補充:
在上面這個例子中,創建事件對象時,第二個參數我設置的是FALSE,也就是說將事件自動重置。後來我自己改用
設置為TRUE,結果出了問題。後來想起來設置為TRUE的話,需要我們手動設置。於是在WaitForSingleObject函數
後面加了ResetEvent函數。本以為這樣就可以解決問題了,但還是有問題。後來在網上問了下別人也找了點書看才知
道了原因。
這種做法存在兩個問題,一個問題是,在單CPU平台下,同一時刻只能有一個線程在運行,假設線程ThreadForWrite
先執行,它得到事件對象:hEvent,但是如果正好這時它的時間片終止了,於是輪到線程ThreadForRead執行,但因
為現在在線程ThreadForWrite中,ResetEvent函數還沒有被執行,所以該事件對象仍然處於“標志的”狀態,因此線程
ThreadForRead就可以得到該事件對象,也就是說,此時兩個線程都可以訪問共享資源,於是結果就無法預料了。
第二個問題,當把這段程序移植到多CPU平台上時,兩個線程就可以同時運行,這時再主函數裡調用SetEvent函數將
其設置為”標志的“狀態已經沒多大意義了,因為這兩個線程都已經可以訪問共享資源了,而且是同時使用。
看來以後實現線程同步時還是老實點使用自動重置的事件對象。