Delphi中的線程類(4)

 

臨界區(CriticalSection)則是一項共享數據訪問保護的技術。它其實也是相當於一個全局的布爾變量。但對它的操作有所不同,它只有兩個操作:EnterLeave,同樣可以把它的兩個狀態當作TrueFalse,分別表示現在是否處於臨界區中。這兩個操作也是原語,所以它可以用於在多線程應用中保護共享數據,防止訪問衝突。

用臨界區保護共享數據的方法很簡單:在每次要訪問共享數據之前調用Enter設置進入臨界區標誌,然後再操作數據,最後調用Leave離開臨界區。它的保護原理是這樣的:當一個線程進入臨界區後,如果此時另一個線程也要訪問這個數據,則它會在調用Enter時,發現已經有線程進入臨界區,然後此線程就會被掛起,等待當前在臨界區的線程調用Leave離開臨界區,當另一個線程完成操作,調用Leave離開後,此線程就會被喚醒,並設置臨界區標誌,開始操作數據,這樣就防止了訪問衝突。

以前面那個InterlockedIncrement爲例,我們用CriticalSectionWindows API)來實現它:

Var

  InterlockedCrit : TRTLCriticalSection;

Procedure InterlockedIncrement( var aValue : Integer );

Begin

  EnterCriticalSection( InterlockedCrit );

  Inc( aValue );

  LeaveCriticalSection( InterlockedCrit );

End;

現在再來看前面那個例子:

1.         線程A進入臨界區(假設數據爲3

2.         線程B進入臨界區,因爲A已經在臨界區中,所以B被掛起

3.         線程A對數據加一(現在是4

4.         線程A離開臨界區,喚醒線程B(現在內存中的數據是4

5.         線程B被喚醒,對數據加一(現在就是5了)

6.         線程B離開臨界區,現在的數據就是正確的了。

臨界區就是這樣保護共享數據的訪問。

關於臨界區的使用,有一點要注意:即數據訪問時的異常情況處理。因爲如果在數據操作時發生異常,將導致Leave操作沒有被執行,結果將使本應被喚醒的線程未被喚醒,可能造成程序的沒有響應。所以一般來說,如下面這樣使用臨界區纔是正確的做法:

EnterCriticalSection

Try

   //  操作臨界區數據

Finally

  LeaveCriticalSection

End;

 

最後要說明的是,EventCriticalSection都是操作系統資源,使用前都需要創建,使用完後也同樣需要釋放。如TThread類用到的一個全局EventSyncEvent和全局CriticalSectionTheadLock,都是在InitThreadSynchronizationDoneThreadSynchronization中進行創建和釋放的,而它們則是在Classes單元的InitializationFinalization中被調用的。

由於在TThread中都是用API來操作EventCriticalSection的,所以前面都是以API爲例,其實Delphi已經提供了對它們的封裝,在SyncObjs單元中,分別是TEvent類和TCriticalSection類。用法也與前面用API的方法相差無幾。因爲TEvent的構造函數參數過多,爲了簡單起見,Delphi還提供了一個用默認參數初始化的Event類:TSimpleEvent

順便再介紹一下另一個用於線程同步的類:TMultiReadExclusiveWriteSynchronizer,它是在SysUtils單元中定義的。據我所知,這是Delphi RTL中定義的最長的一個類名,還好它有一個短的別名:TMREWSync。至於它的用處,我想光看名字就可以知道了,我也就不多說了。

 

有了前面對EventCriticalSection的準備知識,可以正式開始討論SynchronizeWaitFor了。

 

我們知道,Synchronize是通過將部分代碼放到主線程中執行來實現線程同步的,因爲在一個進程中,只有一個主線程。先來看看Synchronize的實現:

procedure TThread.Synchronize(Method: TThreadMethod);

begin

  FSynchronize.FThread := Self;

  FSynchronize.FSynchronizeException := nil;

  FSynchronize.FMethod := Method;

  Synchronize(@FSynchronize);

end;

其中FSynchronize是一個記錄類型:

  PSynchronizeRecord = ^TSynchronizeRecord;

  TSynchronizeRecord = record

    FThread: TObject;

    FMethod: TThreadMethod;

    FSynchronizeException: TObject;

  end;

用於進行線程和主線程之間進行數據交換,包括傳入線程類對象,同步方法及發生的異常。

Synchronize中調用了它的一個重載版本,而且這個重載版本比較特別,它是一個“類方法”。所謂類方法,是一種特殊的類成員方法,它的調用並不需要創建類實例,而是像構造函數那樣,通過類名調用。之所以會用類方法來實現它,是因爲爲了可以在線程對象沒有創建時也能調用它。不過實際中是用它的另一個重載版本(也是類方法)和另一個類方法StaticSynchronize。下面是這個Synchronize的代碼:

class procedure TThread.Synchronize(ASyncRec: PSynchronizeRecord);

var

  SyncProc: TSyncProc;

begin

  if GetCurrentThreadID = MainThreadID then

    ASyncRec.FMethod

  else

  begin

    SyncProc.Signal := CreateEvent(nil, True, False, nil);

    try

      EnterCriticalSection(ThreadLock);

      try

        if SyncList = nil then

          SyncList := TList.Create;

        SyncProc.SyncRec := ASyncRec;

        SyncList.Add(@SyncProc);

        SignalSyncEvent;

        if Assigned(WakeMainThread) then

          WakeMainThread(SyncProc.SyncRec.FThread);

        LeaveCriticalSection(ThreadLock);

        try

          WaitForSingleObject(SyncProc.Signal, INFINITE);

        finally

          EnterCriticalSection(ThreadLock);

        end;

      finally

        LeaveCriticalSection(ThreadLock);

      end;

    finally

      CloseHandle(SyncProc.Signal);

    end;

    if Assigned(ASyncRec.FSynchronizeException) then raise ASyncRec.FSynchronizeException;

  end;

end;

這段代碼略多一些,不過也不算太複雜。

首先是判斷當前線程是否是主線程,如果是,則簡單地執行同步方法後返回。

如果不是主線程,則準備開始同步過程。

通過局部變量SyncProc記錄線程交換數據(參數)和一個Event Handle,其記錄結構如下:

  TSyncProc = record

    SyncRec: PSynchronizeRecord;

    Signal: THandle;

  end;

然後創建一個Event,接着進入臨界區(通過全局變量ThreadLock進行,因爲同時只能有一個線程進入Synchronize狀態,所以可以用全局變量記錄),然後就是把這個記錄數據存入SyncList這個列表中(如果這個列表不存在的話,則創建它)。可見ThreadLock這個臨界區就是爲了保護對SyncList的訪問,這一點在後面介紹CheckSynchronize時會再次看到。

再接下就是調用SignalSyncEvent,其代碼在前面介紹TThread的構造函數時已經介紹過了,它的功能就是簡單地將SyncEvent作一個Set的操作。關於這個SyncEvent的用途,將在後面介紹WaitFor時再詳述。

接下來就是最主要的部分了:調用WakeMainThread事件進行同步操作。WakeMainThread是一個TNotifyEvent類型的全局事件。這裏之所以要用事件進行處理,是因爲Synchronize方法本質上是通過消息,將需要同步的過程放到主線程中執行,如果在一些沒有消息循環的應用中(如ConsoleDLL)是無法使用的,所以要使用這個事件進行處理。

而響應這個事件的是Application對象,下面兩個方法分別用於設置和清空WakeMainThread事件的響應(來自Forms單元):

procedure TApplication.HookSynchronizeWakeup;

begin

  Classes.WakeMainThread := WakeMainThread;

end;

 

procedure TApplication.UnhookSynchronizeWakeup;

begin

  Classes.WakeMainThread := nil;

end;

上面兩個方法分別是在TApplication類的構造函數和析構函數中被調用。

這就是在Application對象中WakeMainThread事件響應的代碼,消息就是在這裏被髮出的,它利用了一個空消息來實現:

procedure TApplication.WakeMainThread(Sender: TObject);

begin

  PostMessage(Handle, WM_NULL, 0, 0);

end;

而這個消息的響應也是在Application對象中,見下面的代碼(刪除無關的部分):

procedure TApplication.WndProc(var Message: TMessage);

begin

  try

    with Message do

      case Msg of

        WM_NULL:

          CheckSynchronize;

  except

    HandleException(Self);

  end;

end;

其中的CheckSynchronize也是定義在Classes單元中的,由於它比較複雜,暫時不詳細說明,只要知道它是具體處理Synchronize功能的部分就好,現在繼續分析Synchronize的代碼。

在執行完WakeMainThread事件後,就退出臨界區,然後調用WaitForSingleObject開始等待在進入臨界區前創建的那個Event。這個Event的功能是等待這個同步方法的執行結束,關於這點,在後面分析CheckSynchronize時會再說明。

注意在WaitForSingleObject之後又重新進入臨界區,但沒有做任何事就退出了,似乎沒有意義,但這是必須的!

因爲臨界區的EnterLeave必須嚴格的一一對應。那麼是否可以改成這樣呢:

        if Assigned(WakeMainThread) then

          WakeMainThread(SyncProc.SyncRec.FThread);

        WaitForSingleObject(SyncProc.Signal, INFINITE);

      finally

        LeaveCriticalSection(ThreadLock);

      end;

上面的代碼和原來的代碼最大的區別在於把WaitForSingleObject也納入臨界區的限制中了。看上去沒什麼影響,還使代碼大大簡化了,但真的可以嗎?

事實上是不行!

因爲我們知道,在Enter臨界區後,如果別的線程要再進入,則會被掛起。而WaitFor方法則會掛起當前線程,直到等待別的線程SetEvent後纔會被喚醒。如果改成上面那樣的代碼的話,如果那個SetEvent的線程也需要進入臨界區的話,死鎖(Deadlock)就發生了(關於死鎖的理論,請自行參考操作系統原理方面的資料)。

死鎖是線程同步中最需要注意的方面之一!

最後釋放開始時創建的Event,如果被同步的方法返回異常的話,還會在這裏再次拋出異常。

(待續)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章