在說數據同步控制之前,先提一個小小的概念,線程安全函數,我們在多線程開發工作中,常常要面對這個概念,究竟什麼是線程安全函數,我的理解是該函數在多線程環境中輸出結果是穩定的(結果唯一),原來c運行庫中沒有考慮到多線程環境,因此有一些線程不安全函數:strtok,_wcstok,asctime,_strerror等,爲了解決這個問題後來推出了多線程C運行庫。從本質上來講,造成這些問題的根本原因是多線程對一些共享資源的訪問是非原子操作(非獨佔操作),因此,要解決這個問題就是我們今天要說的數據同步控制;
用戶態同步控制方法:
1)原子操作:互鎖的函數家族
InterlockedExchangeAdd(PLONG plAdend, LONG lIncement);
InterlockedExchangePoint(PVOID* ppvTarget,PVOID pvValue);
2) 關鍵代碼段(用戶態同步)
CRITICAL_SECTION g_cs;
InitializeCriticalSection(&g_cs);
EnterCriticalSection(&g_cs);
.......
LeaveCriticalSection(&g_cs);
注意點:
(1) EnterCriticalSection(&g_cs)函數阻塞時間是由註冊表中(HLM/system/CurrentControlSet/Control/Session Manager)的CriticalSectionTimeout決定;
(2)對於多個共享資源的訪問,使用多個關鍵代碼段,函數中調用EnterCriticalSection的順序相同,否則有死鎖的可能;
(3) TryEnterCriticalSection(&g_cs)該函數不允許調用線程進入等待狀態;它立即返回表明資源佔用情況,被佔用返回FALSE,否則返回TRUE;
關鍵代碼與循環鎖
如果線程試圖佔有另一個線程擁有的關鍵代碼段時,調用線程就立即置爲等待狀態;線程必須從用戶態轉入爲內核態(大約1000個CPU週期),是很大代價的;爲了克服這種直接轉入內核態所帶來的大代價,引入了循環鎖,因此當EnterCriticalSection函數被調用時,它先使用循環鎖進行循環,以便多次取得該資源,只有所有嘗試失敗後,該線程才進入等待狀態;目前對多處理器機器纔有改善性能,單處理器循環鎖沒有任何性能改進,因爲在循環過程中,擁有資源的線程根本無法釋放;
引入循環鎖的關鍵代碼函數
(1)InitalizeCriticalSectionAndSpinCount(&g_cs,1000);//如果單處理器的話,1000會被忽略成0;
改變關鍵代碼段循環次數
(2)SetCriticalSectionSpinCount(&g_cs,10000);
(3)關鍵代碼的錯誤處理
建議使用結構化異常處理方法;
上面介紹的都是用戶態的同步方式,其侷限性1)互鎖家族函數只能對單值進行操作,根本不能使線程進入等待狀態;2)關鍵代碼段速度快,能使線程進入等待狀態,但是無法設置等待超時,容易使之陷入死鎖狀態;下面介紹內核對象的同步方式
常見的內核對象
1)作業(job),進程,線程
2)文件,文件修改通知
3)互斥對象,事件,信號,可等待的定時器
等待函數 WaitForSingleObject(handle,timeout),WaitForMultipleObject(...)
等待函數可以使線程自願進入等待狀態,直到一個特定的內核對象爲已通知狀態爲止;
等待的副作用
就是等待成功後會改變內核對象的當前通知,將內核對象置爲未通知狀態;等待的副作用是有用的,它保證多個線程等待同一內核對象時只從中喚醒一個,至於是哪一個,那就去問問microsoft啦:)
作業,進程,線程
一個布爾值表示其狀態,運行時爲未通知狀態,終止時爲通知狀態
事件
比線程稍微複雜,其包含兩個布爾值,除了通知狀態外,還有自動重置標誌;當自動重置的事件得到通知時,等待該事件的所有線程中一個線程變爲可調度線程,當人工重置的事件得到通知時,等待該事件的所有線程均變爲可調度線程(全部激活);
CreateEvent(...),OpenEvent(...),CloseHandle(...),SetEvent(...),ResetEvent(...);
等待定時器
在某個時間或按規定時間間隔發出自己的信號通知的內核對象;
CreateWaitableTimer(),SetWaitableTimer()(只有調用這個函數才能設置定時器的通知發出)
信號內核對象
除了使用計數外,信號還包含兩個帶符號的32位值,一個是最大資源數量,一個是當前資源數量;其使用規則:
1)如果當前資源數量大於0時,信號發出通知;
2)如果當前資源數量等於0時,則不發出信號通知;
3)系統決不允許當前資源數量爲負值;
4)當前資源數量決不能大於最大資源數量;
CreateSemaphore(...);//創建一個信號對象;
ReleaseSemaphore(...);//使信號當前使用資源數量進行遞增;
等待信號的副作用是使信號的當前使用資源數遞減1,這正是我們希望得到的:);
互斥對象
顧名思義,互斥對象就是確保線程擁有對單個資源的互斥訪問權;其行爲特性跟關鍵代碼段一樣;
CreateMetux(...)//創建一個互斥對象;
OpenMetux(...);//打開一個互斥對象;
ReleaseMetux(...);//釋放一個互斥對象;
整個線程同步控制就介紹這麼,下面舉一個簡單例子,利用互斥對象和信號對象實現一個線程安全的queue對象;
class CQueue{
public:
struct ELEMENT{
....
};
private:
PELEMENT m_pElements;// array of elements to be processed;
int m_nMaxElements;
HANDLE m_h[2];//metux & semphore handles;
HANDLE &m_hmtx0;
HANDLE &m_hsemNumElements;
public:
CQueue();
~CQueue();
BOOL Apend(PELEMENT pElement,DWORD dwTimeout);
BOOL Remove(PELEMENT pElement,DWORD dwTimeout);
};
BOOL CQueue::Append(PELEMENT pElement,DWORD dwTimeout)
{
DWORD dw = WaitForSingleObject(m_hmtx0,dwTimeout);
if(dw == WAIT_OBJECT_0)
{
LONG lPrevCount;
fOK = ReleaseSemphore(m_hsemNumElements,1,&lPrevCount);
........
Release(m_hmtx0);
}
return TRUE;
}
BOOL CQueue::Remove(PELEMENT pElement,DWORD dwTimeout)
{
BOOL fok=(WaitForMutipleObjects(2,m_h,TRUE,dwTimeout) == WAIT_OBJECT_0);
if(fok)
{
*pElements=m_pElements[0];
MoveMemory(&m_pElements[0],&m_pElements[1],sizeof(ELEMENT)*(m_nMaxElements -1));
ReleaseMetux(m_hmtx0);
}
return TRUE;
}
that's over! : -)