{----------------------------------------------------------------------------- The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/MPL-1.1.html Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is: MTThreading.pas, released on 2000-09-22. The Initial Developer of the Original Code is Erwin Molendijk. Portions created by Erwin Molendijk are Copyright (C) 2002 Erwin Molendijk. All Rights Reserved. Contributor(s): ______________________________________. You may retrieve the latest version of this file at the Project JEDI home page, located at http://www.delphi-jedi.org Known Issues: -----------------------------------------------------------------------------} // $Id: JvMTThreading.pas 10612 2006-05-19 19:04:09Z jfudickar $ unit JvMTThreading; {$I jvcl.inc} interface uses {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} JclUnitVersioning, {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} SysUtils, Classes, SyncObjs, Contnrs, {$IFDEF MSWINDOWS} Windows, Messages, {$ENDIF MSWINDOWS} {$IFDEF HAS_UNIT_LIBC} Libc, {$ENDIF HAS_UNIT_LIBC} {$IFDEF UNIX} QWindows, {$ENDIF UNIX} JvMTConsts, JvMTSync, JvVCL5Utils; type TMTManager = class; TMTThread = class; TMTEvent = procedure(Thread: TMTThread) of object; {$IFDEF COMPILER5} TIntThread = class(Classes.TThread) public destructor Destroy; override; procedure Synchronize(Method: TThreadMethod); function WaitFor: Longword; end; {$ELSE} TIntThread = TThread; {$ENDIF COMPILER5} TMTInternalThread = class(TIntThread) private FName: string; FOnExecute: TNotifyEvent; protected procedure Execute; override; procedure RaiseName; public property Name: string read FName write FName; property OnExecute: TNotifyEvent read FOnExecute write FOnExecute; end; TMTThread = class(TObject) private FFinished: Boolean; FIntThread: TMTInternalThread; FManager: TMTManager; FName: string; FOnExecute: TMTEvent; FOnFinished: TMTEvent; FOnTerminating: TMTEvent; FReferenceCount: Integer; FStatusChange: TCriticalSection; FTerminateSignal: THandle; FTicket: TMTTicket; procedure CreateAndRun; function GetStatus: TMTThreadStatus; procedure Log(const Msg: string); procedure OnIntThreadExecute(Sender: TObject); procedure OnIntThreadTerminate(Sender: TObject); {$IFDEF COMPILER5} procedure SyncOnIntThreadTerminate; {$ENDIF COMPILER5} procedure SetName(const Value: string); protected procedure DecRef; procedure IncRef; public constructor Create(Manager: TMTManager; Ticket: Integer); destructor Destroy; override; procedure CheckTerminate; procedure Release; procedure Run; procedure Synchronize(Method: TThreadMethod); procedure Terminate; procedure Wait; property Name: string read FName write SetName; property OnExecute: TMTEvent read FOnExecute write FOnExecute; property OnFinished: TMTEvent read FOnFinished write FOnFinished; property OnTerminating: TMTEvent read FOnTerminating write FOnTerminating; property ReferenceCount: Integer read FReferenceCount; property Status: TMTThreadStatus read GetStatus; property TerminateSignal: THandle read FTerminateSignal; property ThreadManager: TMTManager read FManager; property Ticket: TMTTicket read FTicket; end; TMTManager = class(TObject) private FGenTicket: TCriticalSection; FNextTicket: TMTTicket; FThreads: TObjectList; FThreadsChange: TCriticalSection; function FindThread(Ticket: TMTTicket; var Thread: TMTThread): Boolean; function GenerateTicket: TMTTicket; procedure Log(const Msg: string); procedure TryRemoveThread(Thread: TMTThread); function InternalActiveThreads(RaiseID: Longword): Integer; protected procedure OnThreadFinished(Thread: TMTThread); public constructor Create; destructor Destroy; override; function AcquireNewThread: TMTThread; function AcquireThread(Ticket: TMTTicket; var Thread: TMTThread): Boolean; function ActiveThreads: Boolean; procedure ReleaseThread(Ticket: TMTTicket); procedure TerminateThreads; procedure WaitThreads; end; function CurrentMTThread: TMTThread; {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} const UnitVersioning: TUnitVersionInfo = ( RCSfile: '$URL: https://jvcl.svn.sourceforge.net/svnroot/jvcl/tags/JVCL3_32/run/JvMTThreading.pas $'; Revision: '$Revision: 10612 $'; Date: '$Date: 2006-05-19 21:04:09 +0200 (ven., 19 mai 2006) $'; LogPath: 'JVCL\run' ); {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} implementation {$IFDEF USEJVCL} uses JvResources; {$ENDIF USEJVCL} {$IFNDEF USEJVCL} resourcestring RsECurThreadIsPartOfManager = 'Current MTThread is part of the MTManager'; RsECheckTerminateCalledByWrongThread = 'CheckTerminate can only be called by the same thread'; RsEThreadNotInitializedOrWaiting = 'Cannot run: thread is not Initializing or Waiting'; RsECannotChangeNameOfOtherActiveThread = 'Cannot change name of other active thread'; RsEReleaseOfUnusedTicket = 'Release of unused ticket'; {$ENDIF !USEJVCL} threadvar _CurrentMTThread: TMTThread; function CurrentMTThread: TMTThread; begin Result := _CurrentMTThread; end; {$IFDEF COMPILER5} type PSyncRequest = ^TSyncRequest; TSyncRequest = record Method: TThreadMethod; ExceptionObject: TObject; Signal: THandle; end; var SyncRequestAvailable: Boolean; ThreadSyncLock: TRTLCriticalSection; SyncRequestList: TList = nil; SyncWindow: HWND; function CheckSynchronize: Boolean; var SyncRequest: PSyncRequest; begin Result := False; // Only the main thread is allowed to synchronize thread methods. if GetCurrentThreadID <> MainThreadID then Exit; EnterCriticalSection(ThreadSyncLock); try if SyncRequestAvailable and (SyncRequestList <> nil) then begin // Do not block while another thread is adding a new synchronization request. while SyncRequestList.Count > 0 do begin SyncRequest := SyncRequestList[0]; SyncRequestList.Delete(0); try SyncRequest.Method; except SyncRequest^.ExceptionObject := ExceptObject; end; // inform TIntThread.Synchronize SetEvent(SyncRequest.Signal); SyncRequestAvailable := False; Result := True; end; end; finally LeaveCriticalSection(ThreadSyncLock); end; end; procedure FinalizeSyncRequestList; begin // if the list is not empty there are still waiting threads if SyncRequestList <> nil then begin CheckSynchronize; SyncRequestList.Free; SyncRequestList := nil; end; end; function SyncWndProc(wnd: HWND; Msg: Cardinal; wParam, lParam: Integer): Integer; stdcall; begin if Msg = WM_USER + 1 then Result := Integer(CheckSynchronize) else Result := DefWindowProc(wnd, Msg, wParam, lParam); end; //=== { TIntThread } ========================================================= procedure TIntThread.Synchronize(Method: TThreadMethod); var SyncRequest: TSyncRequest; begin if GetCurrentThreadID = MainThreadID then Method else begin SyncRequest.Signal := CreateEvent(nil, True, False, nil); try EnterCriticalSection(ThreadSyncLock); try if SyncRequestList = nil then SyncRequestList := TList.Create; SyncRequest.ExceptionObject := nil; SyncRequest.Method := Method; // The function returns only when the item is deleted from the List. SyncRequestList.Add(@SyncRequest); SyncRequestAvailable := True; finally LeaveCriticalSection(ThreadSyncLock); end; PostMessage(SyncWindow, WM_USER + 1, 0, 0); // Wait for CheckSynchronize. WaitForSingleObject(SyncRequest.Signal, INFINITE); finally CloseHandle(SyncRequest.Signal); end; // An exception occured. Re-raise it in the calling thread's context. if Assigned(SyncRequest.ExceptionObject) then raise SyncRequest.ExceptionObject; end; end; destructor TIntThread.Destroy; begin CheckSynchronize; inherited Destroy; end; function TIntThread.WaitFor: Longword; begin CheckSynchronize; Result := inherited WaitFor; end; {$ENDIF COMPILER5} //=== { TMTInternalThread } ================================================== procedure TMTInternalThread.Execute; begin RaiseName; if Assigned(FOnExecute) then FOnExecute(Self); end; procedure TMTInternalThread.RaiseName; {$IFDEF COMPILER7_UP} var ThreadNameInfo: TThreadNameInfo; {$ENDIF COMPILER7_UP} begin {$IFDEF COMPILER7_UP} ThreadNameInfo.FType := $1000; ThreadNameInfo.FName := PChar(FName); ThreadNameInfo.FThreadID := $FFFFFFFF; ThreadNameInfo.FFlags := 0; try RaiseException($406D1388, 0, SizeOf(ThreadNameInfo) div SizeOf(Longword), @ThreadNameInfo); except end; {$ENDIF COMPILER7_UP} end; //=== { TMTThread } ========================================================== constructor TMTThread.Create(Manager: TMTManager; Ticket: Integer); begin inherited Create; FStatusChange := TCriticalSection.Create; FManager := Manager; FTicket := Ticket; FName := 'MT' + IntToStr(Ticket); // do not localize FTerminateSignal := CreateSemaphore(nil, 0, 1, ''); end; destructor TMTThread.Destroy; begin CloseHandle(FTerminateSignal); FStatusChange.Free; inherited Destroy; end; procedure TMTThread.CheckTerminate; begin if CurrentMTThread <> Self then raise EMTThreadError.CreateRes(@RsECheckTerminateCalledByWrongThread); if Status = tsTerminating then raise EMTTerminateError.Create(''); end; procedure TMTThread.CreateAndRun; begin FStatusChange.Acquire; try FIntThread := TMTInternalThread.Create(True); FIntThread.OnExecute := OnIntThreadExecute; FIntThread.OnTerminate := OnIntThreadTerminate; FIntThread.FreeOnTerminate := True; FIntThread.Name := FName; FIntThread.Resume; finally FStatusChange.Release; end; end; procedure TMTThread.DecRef; begin InterlockedDecrement(FReferenceCount); end; function TMTThread.GetStatus: TMTThreadStatus; begin FStatusChange.Acquire; try if FFinished then Result := tsFinished else if FIntThread = nil then Result := tsInitializing else if FIntThread.Suspended then Result := tsWaiting else if FIntThread.Terminated then Result := tsTerminating else Result := tsRunning; finally FStatusChange.Release; end; end; procedure TMTThread.IncRef; begin InterlockedIncrement(FReferenceCount); end; procedure TMTThread.Log(const Msg: string); begin // (rom) no OutputDebugString in production code {$IFDEF DEBUGINFO_ON} OutputDebugString(PChar('[' + ClassName + '] ' + Msg)); {$ENDIF DEBUGINFO_ON} end; procedure TMTThread.OnIntThreadExecute(Sender: TObject); begin // set the CurrentMTThread variable. // this variable is global, but only to this thread. _CurrentMTThread := Self; // run OnExecute event try if Assigned(FOnExecute) then FOnExecute(Self); except on E: EMTTerminateError do {nothing}; on E: Exception do Log('OnExecute Exception: "' + E.Message + '"'); // do not localize end; // make sure terminate flag is set FIntThread.Terminate; // run OnTerminating event try if Assigned(FOnTerminating) then FOnTerminating(Self); except on E: Exception do Log('OnTerminate Exception: "' + E.Message + '"'); // do not localize end; {$IFDEF COMPILER5} FIntThread.OnTerminate := nil; Synchronize(SyncOnIntThreadTerminate); {$ENDIF COMPILER5} end; {$IFDEF COMPILER5} procedure TMTThread.SyncOnIntThreadTerminate; begin OnIntThreadTerminate(Self); end; {$ENDIF COMPILER5} procedure TMTThread.OnIntThreadTerminate(Sender: TObject); begin FStatusChange.Acquire; try if FFinished then Exit; FFinished := True; finally FStatusChange.Release; end; if Assigned(FOnFinished) then FOnFinished(Self); FStatusChange.Acquire; try FIntThread := nil; finally FStatusChange.Release; end; // After a call to OnThreadFinished, this object might be destroyed. // So don't access any fields after this call. FManager.OnThreadFinished(Self); end; procedure TMTThread.Release; begin FManager.ReleaseThread(FTicket); end; procedure TMTThread.Run; begin FStatusChange.Acquire; try if Status = tsInitializing then CreateAndRun else if Status = tsWaiting then FIntThread.Resume else raise EMTThreadError.CreateRes(@RsEThreadNotInitializedOrWaiting); finally FStatusChange.Release; end; end; procedure TMTThread.SetName(const Value: string); begin FStatusChange.Acquire; try if Status in [tsInitializing, tsFinished] then FName := Value else begin if CurrentMTThread <> Self then raise EMTThreadError.CreateRes(@RsECannotChangeNameOfOtherActiveThread); FName := Value; if FIntThread <> nil then begin FIntThread.Name := FName; FIntThread.RaiseName; end; end; finally FStatusChange.Release; end; end; procedure TMTThread.Synchronize(Method: TThreadMethod); begin if CurrentMTThread = Self then FIntThread.Synchronize(Method) else if CurrentMTThread = nil then Method else CurrentMTThread.Synchronize(Method); end; procedure TMTThread.Terminate; begin if Status in [tsTerminating, tsFinished] then Exit; FStatusChange.Acquire; try if FIntThread <> nil then FIntThread.Terminate {thread was Running} else FFinished := True; {thread was initializing} // make sure thread escapes from any Wait() calls ReleaseSemaphore(FTerminateSignal, 1, nil); finally FStatusChange.Release; end; end; procedure TMTThread.Wait; var SelfRef: TMTThread; begin if FManager.AcquireThread(Ticket, SelfRef) then try if GetCurrentThreadID = MainThreadID then begin while Status <> tsFinished do begin CheckSynchronize; Sleep(1); end; end else begin while Status <> tsFinished do Sleep(1); end; finally Release; end; end; //=== { TMTManager } ========================================================= constructor TMTManager.Create; begin inherited Create; FGenTicket := TCriticalSection.Create; FThreadsChange := TCriticalSection.Create; FThreads := TObjectList.Create(True); end; destructor TMTManager.Destroy; var I: Integer; begin // set the terminate flag at each thread TerminateThreads; // wait for them to finish WaitThreads; FThreadsChange.Acquire; try for I := 0 to FThreads.Count-1 do Log('Unreleased thread: "' + TMTThread(FThreads[I]).Name + '"'); // do not localize finally FThreadsChange.Release; end; FThreads.Free; FThreadsChange.Free; FGenTicket.Free; inherited Destroy; end; function TMTManager.AcquireNewThread: TMTThread; begin Result := TMTThread.Create(Self, GenerateTicket); try Result.IncRef; FThreadsChange.Acquire; try FThreads.Add(Result); finally FThreadsChange.Release; end; except Result.Free; raise; end; end; function TMTManager.AcquireThread(Ticket: TMTTicket; var Thread: TMTThread): Boolean; begin FThreadsChange.Acquire; try Result := FindThread(Ticket, Thread); if Result then Thread.IncRef; finally FThreadsChange.Release; end; end; // returns 0 = False // 1 = True // -1 = RaiseID found and active function TMTManager.InternalActiveThreads(RaiseID: Longword): Integer; var I: Integer; begin Result := 0; FThreadsChange.Acquire; try for I := 0 to FThreads.Count - 1 do if TMTThread(FThreads[I]).Status <> tsFinished then begin if (RaiseID <> 0) and (TMTThread(FThreads[I]).FIntThread.ThreadID = RaiseID) then Result := -1 // no Break; here: Return -1 only when RaiseID is the last active thread else begin Result := 1; Break; end; end; finally FThreadsChange.Release; end; end; function TMTManager.ActiveThreads: Boolean; begin Result := InternalActiveThreads(0) <> 0; end; function TMTManager.FindThread(Ticket: TMTTicket; var Thread: TMTThread): Boolean; var I: Integer; begin FThreadsChange.Acquire; try I := FThreads.Count-1; while (I <> -1) and (TMTThread(FThreads[I]).Ticket <> Ticket) do Dec(I); Result := I <> -1; if Result then Thread := TMTThread(FThreads[I]) else Thread := nil; finally FThreadsChange.Release; end; end; function TMTManager.GenerateTicket: TMTTicket; begin FGenTicket.Acquire; try Result := FNextTicket; Inc(FNextTicket); finally FGenTicket.Release; end; end; procedure TMTManager.Log(const Msg: string); begin // (rom) no OutputDebugString in production code {$IFDEF DEBUGINFO_ON} OutputDebugString(PChar('[' + ClassName + '] ' + Msg)); {$ENDIF DEBUGINFO_ON} end; procedure TMTManager.OnThreadFinished(Thread: TMTThread); begin TryRemoveThread(Thread); end; procedure TMTManager.ReleaseThread(Ticket: TMTTicket); var Thread: TMTThread; begin FThreadsChange.Acquire; try if FindThread(Ticket, Thread) then Thread.DecRef else raise EMTThreadError.CreateRes(@RsEReleaseOfUnusedTicket); // if this was the last reference then the thread must be removed TryRemoveThread(Thread); finally FThreadsChange.Release; end; end; procedure TMTManager.TerminateThreads; var I: Integer; begin FThreadsChange.Acquire; try for I := 0 to FThreads.Count-1 do TMTThread(FThreads[I]).Terminate; finally FThreadsChange.Release; end; end; procedure TMTManager.TryRemoveThread(Thread: TMTThread); begin FThreadsChange.Acquire; try if (Thread.Status = tsFinished) and (Thread.ReferenceCount = 0) then FThreads.Remove(Thread); finally FThreadsChange.Release; end; end; // wait until the threads are all finished procedure TMTManager.WaitThreads; begin // running from inside the main VCL thread? if GetCurrentThreadID = MainThreadID then begin // use CheckSynchronise to process the OnFinished events while ActiveThreads do begin CheckSynchronize; Sleep(1); end; end else begin //running in a MTThread, just wait for all threads to finish while True do begin case InternalActiveThreads(GetCurrentThreadID) of 0: Break; 1: { Nothing }; -1: raise EMTThreadError.CreateRes(@RsECurThreadIsPartOfManager); end; Sleep(1); end; end; end; {$IFDEF COMPILER5} var SyncWindowClass: TWndClass = ( style: 0; lpfnWndProc: @SyncWndProc; cbClsExtra: 0; cbWndExtra: 0; hInstance: 0; hIcon: 0; hCursor: 0; hbrBackground: 0; lpszMenuName: nil; lpszClassName: 'JvMTThreadingSyncWindow'); procedure CreateSyncWindow; begin RegisterClass(SyncWindowClass); SyncWindow := CreateWindowEx(WS_EX_TOOLWINDOW, SyncWindowClass.lpszClassName, '', WS_POPUP, 0, 0, 0, 0, 0, 0, HInstance, nil); end; {$ENDIF COMPILER5} initialization {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} RegisterUnitVersion(HInstance, UnitVersioning); {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} {$IFDEF COMPILER5} InitializeCriticalSection(ThreadSyncLock); CreateSyncWindow; {$ENDIF COMPILER5} finalization {$IFDEF COMPILER5} FinalizeSyncRequestList; DeleteCriticalSection(ThreadSyncLock); DestroyWindow(SyncWindow); SyncWindow := 0; {$ENDIF COMPILER5} {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} UnregisterUnitVersion(HInstance); {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} end.