Win32下兩種用于C++的線程同步類(上)

線程同步是多線程程序設計的核心內容,它的目的是正確處理多線程並發時的各種問題,例如線程的等待、多個線程訪問同一數據時的互斥,防死鎖等。Win32提供多種內核對象和手段用于線程同步,如互斥量、信號量、事件、臨界區等。所不同的是,互斥量、信號量、事件都是Windows的內核對象,當程序對這些對象進行控制時會自動轉換到核心態,而臨界區本身不是內核對象,它是工作在用戶態的。我們知道從用戶態轉換到核心態是需要以時間爲代價的,所以假如能在用戶態就簡單解決的問題,就可以不必勞煩核心態了。

這裏我要說的是兩種用于C++的多線程同步類,通過對這兩種類的使用就可以方便的實現對變量或代碼段的加鎖控制,從而防止多線程對變量不正確的操作。

所謂加鎖,就是說當我們要訪問某要害變量之前,都需要首先獲得答應才能繼續,假如未獲得答應則只有等待。一個要害變量擁有一把鎖,一個線程必須先得到這把鎖(其實稱爲鑰匙可能更形象)才可以訪問這個變量,而當某個變量持有這把鎖的時候,其他線程就不能重複的得到它,只有等持有鎖的線程把鎖歸還以後其他線程才有可能得到它。之所以這樣做,就是爲了防止一個線程讀取某對象途中另一線程對它進行了修改,或兩線程同時對一變量進行修改,例如:

// 全局:

strUCt MyStruct ... { int a, b; } ;

MyStruct s;

// 線程1:

int a = s.a;

int b = s.b;

// 線程2:

s.a ++ ;

s.b -- ;

假如實際的執行順序就是上述書寫的順序那到沒有什麽,但假如線程2的執行打斷了線程1,變爲如下順序:

int a = s.a; //線程1

s.a++; //線程2

s.b++; //線程2

int b = s.b; //線程1

那麽這時線程1讀出來的a和b就會有問題了,因爲a是在修改前讀的,而b是在修改後讀的,這樣讀出來的是不完整的數據,會對程序帶來不可預料的後果。天知道兩個程的調度順序是什麽樣的。爲了防止這種情況的出現,需要對變量s加鎖,也就是當線程1得到鎖以後就可以放心的訪問s,這時假如線程2要修改s,只有等線程1訪問完成以後將鎖釋放才可以,從而保證了上述兩線程交叉訪問變量的情況不會出現。

使用Win32提供的臨界區可以方便的實現這種鎖:

// 全局:

CRITICAL_SECTION cs;

InitializeCriticalSection( & cs);

// 線程1:

EnterCriticalSection( & cs);

int a = s.a;

int b = s.b;

LeaveCriticalSection( & cs);

// 線程2:

EnterCriticalSection( & cs);

s.a ++ ;

s.b -- ;

LeaveCriticalSection( & cs);

// 最後:

DeleteCriticalSection( & cs);

代碼中的臨界區變量(cs)就可以看作是變量s的鎖,當函數EnterCriticalSection返回時,當前線程就獲得了這把鎖,之後就是對變量的訪問了。訪問完成後,調用LeaveCriticalSection表示釋放這把鎖,答應其他線程繼續使用它。

假如每當需要對一個變量進行加鎖時都需要做這些操作,顯得有些麻煩,而且變量cs與s只有邏輯上的鎖關系,在語法上沒有什麽聯系,這對于鎖的治理帶來了不小的麻煩。程序員總是最懶的,可以想出各種偷懶的辦法來解決問題,例如讓被鎖的變量與加鎖的變量形成物理上的聯系,使得鎖變量成爲被鎖變量不可分割的一部分,這聽起來是個好主意。

首先想到的是把鎖封閉在一個類裏,讓類的構造函數和析構函數來治理對鎖的初始化和鎖毀動作,我們稱這個鎖爲「實例鎖」:

class InstanceLockBase

... {

CRITICAL_SECTION cs;

protected :

InstanceLockBase() ... { InitialCriticalSection( & cs); }

~ InstanceLockBase() ... { DeleteCriticalSection( & cs); }

} ;

假如熟悉C++,看到這裏一定知道後面我要幹什麽了,對了,就是繼續,因爲我把構造函數和析構函數都聲明爲保護的(protected),這樣唯一的作用就是在子類裏使用它。讓我們的被保護數據從這個類繼續,那麽它們不就不可分割了嗎:

struct MyStruct: public InstanceLockBase

... { … } ;

什麽?結構體還能從類繼續?當然,C++中結構體和類除了成員的默認訪問控制不同外沒有什麽不一樣,class能做的struct也能做。此外,也許你還會問,假如被鎖的是個簡單類型,不能繼續怎麽辦,那麽要麽用一個類對這個簡單類型進行封裝(記得Java裏有int和Integer嗎),要麽只好手工治理它們的聯系了。假如被鎖類已經有了基類呢?沒關系,C++是答應多繼續的,多一個基類也沒什麽。

現在我們的數據裏面已經包含一把鎖了,之後就是要添加加鎖和解鎖的動作,把它們作爲InstanceLockBase類的成員函數再合適不過了:

class InstanceLockBase

... {

CRITICAL_SECTION cs;

void Lock() ... { EnterCriticalSection( & cs); }

void Unlock() ... { LeaveCriticalSection( & cs); }

} ;

看到這裏可能會發現,我把Lock和Unlock函數都聲明爲私有了,那麽如何訪問這兩個函數呢?是的,我們總是需要有一個地方來調用這兩個函數以實現加鎖和解鎖的,而且它們總應該成對出現,但C++語法本身沒能限制我們必須成對的調用兩個函數,假如加完鎖忘了解,那後果是嚴重的。這裏有一個例外,就是C++對于構造函數和析構函數的調用是自動成對的,對了,那就把對Lock和Unlock的調用專門寫在一個類的構造函數和析構函數中:

class InstanceLock

... {

InstanceLockBase * _pObj;

public :

InstanceLock(InstanceLockBase * pObj)

... {

_pObj = pObj; // 這裏會保存一份指向s的指針,用于解鎖

if (NULL != _pObj)

_pObj -> Lock(); // 這裏加鎖

}

~ InstanceLock()

... {

if (NULL != _pObj)

_pObj -> Unlock(); // 這裏解鎖

}

} ;

最後別忘了在類InstanceLockBase中把InstanceLock聲明爲友元,使得它能正確訪問Lock和Unlock這兩個私有函數:

class InstanceLockBase

... {

friend class InstanceLock;

} ;

好了,有了上面的基礎,現在對變量s的加解鎖治理變成了對InstanceLock的實例的生命周期的治理了。假如我們有一個函數ModifyS中要對s進行修改,那麽只要在函數一開始就聲明一個InstaceLock的實例,這樣整個函數就自動對s加鎖,一旦進入這個函數,其他線程就都不能獲得s的鎖了:

void ModifyS()

... {

InstanceLock lock ( & s); // 這裏已經實現加鎖了

// some operations on s

} // 一旦離開lock對象的作用域,自動解鎖

假如是要對某函數中一部分代碼加鎖,只要用一對大括號把它們括起來再聲明一個lock就可以了:

... {

InstanceLock lock ( & s);

// do something …

}

好了,就是這麽簡單。下面來看一個測試。

首先預備一個輸出函數,對我們理解程序有幫助。它會在輸出我們想輸出的內容同時打出行號和時間:

void Say( char * text)

... {

static int count = 0 ;

SYSTEMTIME st;

::GetLocalTime( & st);

printf( " %03d [%02d:%02d:%02d.%03d]%s " , ++ count, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, text);

}

當然,原則上當多線程都調用這個函數時應該對其靜態局部變量count進行加鎖,這裏就省略了。

我們聲明一個非常簡單的被鎖的類型,並生成一個實例:

class MyClass: public InstanceLockBase

... {} ;

MyClass mc;

子線程的任務就是對這個對象加鎖,然後輸出一些信息:

DWord CALLBACK ThreadProc(LPVOID param)

... {

InstanceLock il( & mc);

Say( " in sub thread, lock " );

Sleep( 2000 );

Say( " in sub thread, unlock " );

return 0 ;

}

這裏會輸出兩條信息,一是在剛剛獲得鎖的時間,二是在釋放鎖的時候,中間通過Sleep來延遲2秒。

主線程負責開啓子線程,然後也對mc加鎖:

CreateThread( 0 , 0 , ThreadProc, 0 , 0 , 0 );

... {

InstanceLock il( & mc);

Say( " in main thread, lock " );

Sleep( 3000 );

Say( " in main thread, lock " );

}

運行此程序,得到的輸出如下:

001 [13:43:23.781]in main thread, lock

002 [13:43:26.781]in main thread, lock

003 [13:43:26.781]in sub thread, lock

004 [13:43:28.781]in sub thread, unlock

從其輸出的行號和時間可以清楚的看到兩個線程間的互斥:當主線程恰好首先獲得鎖時,它會延遲3秒,然後釋放鎖,之後子線程才得以繼續進行。這個例子也證實我們的類工作的很好。

總結一下,要使用InstanceLock系列類,要做的就是:

1、讓被鎖類從InstanceLockBase繼續

2、所有要訪問被鎖對象的代碼前面聲明InstanceLock的實例,並傳入被鎖對象的指針。

附:完整源代碼:

#pragma once

#include < windows.h >

class InstanceLock;

class InstanceLockBase

... {

friend class InstanceLock;

CRITICAL_SECTION cs;

void Lock()

... {

::EnterCriticalSection( & cs);

}

void Unlock()

... {

::LeaveCriticalSection( & cs);

}

protected :

InstanceLockBase()

... {

::InitializeCriticalSection( & cs);

}

~ InstanceLockBase()

... {

::DeleteCriticalSection( & cs);

}

} ;

class InstanceLock

... {

InstanceLockBase * _pObj;

public :

InstanceLock(InstanceLockBase * pObj)

... {

_pObj = pObj;

if (NULL != _pObj)

_pObj -> Lock();

}

~ InstanceLock()

... {

if (NULL != _pObj)

_pObj -> Unlock();

}

} ;

· 湖北宜昌三峽壩區水面驚現神秘動物

近日,湖北宜昌,一段視頻在當地熱傳:有網友在三峽壩區拍到神秘動物,體型碩大數米長...

· 什麽是語段?語段的類型以及和句群、段落的區別與聯系是什麽?

句群是最高級的語言單位。 段落(自然段)是章法單位...

· 十八部好看的賭石類小說

以下是十八部(排名不分先後)好看的賭石類小說的簡介,喜歡的朋友可以去搜索書名閱讀...

 
Win32下兩種用于C++的線程同步類(下)
  上一篇中我介紹了一種通過封閉Critical Section對象而方便的使用互斥鎖的方式,文中所有的例子是兩個線程對同一數據一讀一寫,因此需要讓它們在這裏互斥,不能同時訪問。而在實際情況中可能會有更複雜的情況出現...查看完整版>>Win32下兩種用于C++的線程同步類(下)
 
原創win32線程池代碼(WinApi/C++), 健壯, 高效,易用,易于擴展, 可用于任何C++編譯器
//說明, 這段代碼我用了很久, 我刪除了自動調整規模的代碼(因爲他還不成熟)/******************************************************************* Thread Pool For Win32 * VC++ 6, BC++ 5.5(Free), GCC(Free)...查看完整版>>原創win32線程池代碼(WinApi/C++), 健壯, 高效,易用,易于擴展, 可用于任何C++編譯器
 
網絡同步校時UDP服務器端SDK代碼(RFC868/C++/WIN32/SOCKET/UDP)
#pragma warning(disable: 4530)#pragma warning(disable: 4786)#include <map>#include <cassert>#include <iostream>#include <fstream>#include <vector>#include <string>#...查看完整版>>網絡同步校時UDP服務器端SDK代碼(RFC868/C++/WIN32/SOCKET/UDP)
 
網絡同步校時TCP服務器端SDK代碼(RFC868/C++/WIN32/SOCKET/TCP/select)
//以下是一段服務器端SDK代碼, 較簡單, 稍加修改可應用于NT服務程序中//僅供初學者參考, 高手勿入, 謝謝#pragma warning(disable: 4530)#pragma warning(disable: 4786)#include <map>#include <casser...查看完整版>>網絡同步校時TCP服務器端SDK代碼(RFC868/C++/WIN32/SOCKET/TCP/select)
 
Python線程編程的兩種方式
??? Python中如果要使用線程的話,python的lib中提供了兩種方式。一種是函數式,一種是用類來包裝的線程對象。舉兩個簡單的例子希望起到抛磚引玉的作用,關于多線程編程的其他知識例如互斥、信號量、臨界區等請參考p...查看完整版>>Python線程編程的兩種方式