I have 4 threads created at runtime. Each thread enters critical section, changes global variable, exits critical section and shows message dialog with the result. OnThreadTerminate I also have a message dialog. It seems to be random, but still, I sometimes get 3 messages with the result and one saying that thread is terminated. How is it even possible? Win7 x64.
There is my full code:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Buttons, Vcl.ComCtrls,
IdThreadComponent, idHTTP, SyncObjs;
const
THREAD_NAME = 'MyidThreadComponent';
type
TForm1 = class(TForm)
StatusBar1: TStatusBar;
BitBtn1: TBitBtn;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure BitBtn1Click(Sender: TObject);
private
{ Private declarations }
FCriticalSection: TCriticalSection;
FGlobalVariable: integer;
procedure CreateThreads(const ACount: integer; const AStart: boolean);
function GetWebsiteContent(const AURL: string): string;
procedure MyIdThreadComponentOnRunHandler(Sender: TIdThreadComponent);
procedure MyIdThreadComponentOnTerminateHandler(Sender: TIdThreadComponent);
public
{ Public declarations }
property GlobalVariable: integer read FGlobalVariable write FGlobalVariable;
property CriticalSection: TCriticalSection read FCriticalSection;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
FCriticalSection := TCriticalSection.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FreeAndNil(FCriticalSection);
end;
function TForm1.GetWebsiteContent(const AURL: string): string;
var
_MyidHTTP: TidHTTP;
begin
_MyidHTTP := TidHTTP.Create(self);
try
Result := _MyidHTTP.Get(AURL);
finally
FreeAndNil(_MyidHTTP);
end;
end;
procedure TForm1.MyIdThreadComponentOnRunHandler(Sender: TIdThreadComponent);
var
_LocalVariable: integer;
begin
CriticalSection.Acquire;
try
// Safe way to deal with global variables. Only one thread will enter
// CriticalSection at time.
_LocalVariable := GlobalVariable;
_LocalVariable := _LocalVariable * 2;
GlobalVariable := _LocalVariable;
finally
CriticalSection.Release;
end;
ShowMessage(Sender.Name + ' started: ' + IntToStr(_LocalVariable));
Sender.Terminate;
end;
procedure TForm1.MyIdThreadComponentOnTerminateHandler
(Sender: TIdThreadComponent);
begin
ShowMessage(Sender.Name + ' terminated.');
end;
procedure TForm1.BitBtn1Click(Sender: TObject);
begin
GlobalVariable := 1;
CreateThreads(4 { System.CPUCount + 1 } , true);
end;
procedure TForm1.CreateThreads(const ACount: integer; const AStart: boolean);
var
_MyIdThreadComponent: TIdThreadComponent;
i: integer;
begin
if ACount > 0 then
for i := 1 to ACount do
begin
_MyIdThreadComponent := FindComponent(THREAD_NAME + IntToStr(i))
as TIdThreadComponent;
if not Assigned(_MyIdThreadComponent) then
begin
_MyIdThreadComponent := TIdThreadComponent.Create(self);
_MyIdThreadComponent.Name := THREAD_NAME + IntToStr(i);
_MyIdThreadComponent.Tag := i;
_MyIdThreadComponent.OnRun := MyIdThreadComponentOnRunHandler;
_MyIdThreadComponent.OnTerminate :=
MyIdThreadComponentOnTerminateHandler;
{$IFDEF MSWINDOWS}
_MyIdThreadComponent.Priority := tpNormal;
{$ENDIF}
{$IFDEF MACOS}
_MyIdThreadComponent.Priority := 1;
{$ENDIF}
end;
if AStart = true then
if Assigned(_MyIdThreadComponent) then
_MyIdThreadComponent.Start;
end;
end;
end.
Showmessage is not the best way to show the output as its not thread safe. Instead, if you use a memo or other control and wrap it in a synchronize call it will be easier to see the results. I modified your routine to output to a memo, and included the ThreadId before and inside the synchronize call so you can better understand what is happening.
Keep in mind that your threads will not always output in the order you may think they will, it is entirely possible that thread 4 will output before thread 1, even though thread 1 was started first and 4 last.
procedure TForm13.MyIdThreadComponentOnRunHandler(Sender: TIdThreadComponent);
var
_LocalVariable: integer;
_LocalThreadId : Cardinal;
begin
fCriticalSection.Acquire;
try
// Safe way to deal with global variables. Only one thread will enter
// CriticalSection at time.
_LocalVariable := GlobalVariable;
_LocalVariable := _LocalVariable * 2;
GlobalVariable := _LocalVariable;
finally
fCriticalSection.Release;
end;
_LocalThreadId := TThread.CurrentThread.ThreadID;
TThread.Synchronize(TThread.CurrentThread,procedure begin
memo1.Lines.Add(Format('%s Started (%d/%d): %d',[Sender.Name,_LocalThreadId,TThread.CurrentThread.ThreadID,_LocalVariable]));
end);
Sender.Terminate;
end;
procedure TForm13.MyIdThreadComponentOnTerminateHandler
(Sender: TIdThreadComponent);
begin
// note sync call is not needed as this is executed in the context of the main thread.
memo1.Lines.Add(Format('%s terminated. (%d)',[Sender.Name,TThread.CurrentThread.ThreadID]));
end;
Related
I want to make an integer result from a thread. It's just a simple calculation.
My problem, I can not have an "instant" result from it. I have execute more than once, then I got the correct result.
Here the unit form :
unit Unit1;
interface
uses
System.SysUtils,
System.Classes,
Vcl.Controls, Vcl.Forms, Vcl.StdCtrls;
type
TForm1 = class(TForm)
btn1: TButton;
mmo1: TMemo;
procedure btn1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
procedure CaclDone(Sender: TObject);
procedure BeginCal(calA, calB: Integer);
function StartCalc(const calA, calB: Integer): Int64;
end;
var
nOut: Int64;
Form1: TForm1;
implementation
{$R *.dfm}
uses uCalculationThread;
procedure TForm1.BeginCal(calA: Integer; calB: Integer);
var
P: TCalc;
begin
nOut := 0;
P := TCalc.Create;
P.numa := calA;
P.numb := calB;
P.OnTerminate := CaclDone;
P.Start;
end;
procedure TForm1.CaclDone(Sender: TObject);
var
P: TCalc;
begin
P := TCalc(Sender);
nOut := P.iOut;
mmo1.Lines.Add('on thread finished : ' + IntToStr(nOut)); // < here is instant result
end;
function TForm1.StartCalc(const calA, calB: Integer): Int64;
begin
BeginCal(calA, calB);
Result := nOut;
end;
procedure TForm1.btn1Click(Sender: TObject);
var
i: Int64;
begin
i := StartCalc(10, 20);
mmo1.Lines.Add('on function call : ' + IntToStr(i)); // first result always 0
end;
end.
And here is the threading unit
unit uCalculationThread;
interface
uses
System.Classes;
type
TCalc = class(TThread)
private
fiOut: Int64;
fnumA, fNumB: Integer;
protected
procedure Execute; override;
public
property numA: Integer read fnumA write fnumA;
property numb: Integer read fNumB write fNumB;
property iOut: Int64 read fiOut write fiOut;
constructor Create;
end;
implementation
constructor TCalc.Create;
begin
inherited Create(True);
FreeOnTerminate := True;
end;
procedure TCalc.Execute;
begin
fiOut := fnumA + fNumB;
end;
end.
What I need is a simple function to have the result from that thread.
Consider this method:
function TForm1.StartCalc(const calA, calB: Integer): Int64;
begin
BeginCal(calA, calB);
Result := nOut;
end;
It calls BeginCal, which creates a thread and starts its execution. The result of the thread is passed through the OnTerminate event which calls CaclDone and assigns the result to the nOut variable.
Since the OnTerminate event is executed in the main thread, this will happen after the execution of StartCalc. Hence, there will be no valid value for StartCalc to return.
What I need is a simple function to have the result from that thread.
This means that the main thread will have to wait for the thread to finish its execution. That defeats the way the GUI is supposed to behave.
You already have a mechanism for getting the result in the CaclDone method.
The way to handle an async calculation chain in this situation is to do something like this (uses System.Threading):
function MyAdd( a,b : Int64): Int64;
begin
Result := a + b;
end;
procedure TForm1.btn1Click(Sender: TObject);
begin
TTask.Create(
procedure
var
i: Int64;
begin
i := MyAdd(10,20);
TThread.Queue(nil, // Result is presented in the main thread.
procedure
begin
mmo1.Lines.Add('on function call : ' + IntToStr(i));
if (i <> 4) then
mmo1.Lines.Add('on function call : <> 4');
end);
end).Start;
end;
I am using this in one of my solution
My requirement is to clear the queue and kill all the threads gracefully when Stop button is clicked.
For this I created an ObjectList
var
List: TObjectList<TMyConsumerItem>;
begin
{ Create a new List. }
List := TObjectList<TMyConsumerItem>.Create();
Later I made this modification:
procedure TForm1.DoSomeJob(myListItems: TStringList);
...
for i := 1 to cThreadCount do
List.Add(TMyConsumerItem.Create(aQueue, aCounter));
And on Stop button button click I am doing this
for i := 0 to List.Count - 1 do
begin
List.Item[i].Terminate;
end;
aCounter.Free;
aQueue.Free;
While doing this I application is getting hanged. Is this the correct approach or am I missing something?
I am using 10.2 Tokyo
Edit 1:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TMyConsumerItem = class(TThread)
private
FQueue : TThreadedQueue<TProc>;
FSignal : TCountDownEvent;
protected
procedure Execute; override;
public
constructor Create( aQueue : TThreadedQueue<TProc>; aSignal : TCountdownEvent);
end;
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
procedure StopClick(Sender: TObject);
private
{ Private declarations }
List: TObjectList<TMyConsumerItem>;
aQueue: TThreadedQueue<TProc>;
aCounter: TCountDownEvent;
procedure DoSomeJob( myListItems : TStringList);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
SyncObjs, Generics.Collections;
{- Include TMyConsumerItem class here }
procedure TForm1.Button1Click(Sender: TObject);
var
aList : TStringList;
i : Integer;
begin
aList := TStringList.Create;
Screen.Cursor := crHourGlass;
try
for i := 1 to 20 do aList.Add(IntToStr(i));
DoSomeJob(aList);
finally
aList.Free;
Screen.Cursor := crDefault;
end;
end;
procedure TForm1.StopClick(Sender: TObject);
begin
for i := 0 to List.Count - 1 do
begin
List.Item[i].Terminate;
end;
List.Free;
aCounter.WaitFor;
aCounter.Free;
aQueue.Free;
end;
procedure TForm1.DoSomeJob(myListItems: TStringList);
const
cThreadCount = 10;
cMyQueueDepth = 100;
var
i: Integer;
function CaptureJob(const aString: string): TProc;
begin
Result :=
procedure
var
i,j : Integer;
begin
// Do some job with aString
for i := 0 to 1000000 do
j := i;
// Report status to main thread
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add('Job with:'+aString+' done.');
end
);
end;
end;
var
aThread : TThread;
begin
List := TObjectList<TMyConsumerItem>.Create();
List.OwnsObjects := False;
aQueue := TThreadedQueue<TProc>.Create(cMyQueueDepth);
aCounter := TCountDownEvent.Create(cThreadCount);
try
for i := 1 to cThreadCount do
List.Add(TMyConsumerItem.Create(aQueue, aCounter));
for i := 0 to myListItems.Count - 1 do
begin
aQueue.PushItem(CaptureJob(myListItems[i]));
end;
finally
end;
end;
constructor TMyConsumerItem.Create(aQueue: TThreadedQueue<TProc>; aSignal : TCountDownEvent);
begin
Inherited Create(false);
Self.FreeOnTerminate := true;
FQueue := aQueue;
FSignal := aSignal;
end;
procedure TMyConsumerItem.Execute;
var
aProc : TProc;
begin
try
repeat
FQueue.PopItem(aProc);
aProc();
until Terminated;
finally
FSignal.Signal;
end;
end;
end.
You left out some important stuff regarding how the job queue works and how to interact with the threadpool.
Taking a reference to a thread that is self-terminating is wrong. Remove the List, since it is useless.
In order to finish the queue at a later point, make aQueue global.
To finish the threadpool, add as many empty tasks to the queue as there are threads.
See example below how a stop method could be implemented. Note that both aCounter and aQueue must be global in scope. Disclaimer untested, not in front of a compiler at the moment.
If you need to abort ongoing work in the job tasks, you will have to provide a reference to a global (in scope) flag with each job task, and signal to end the task.
There are other libraries that can perform similar work, see Delphi PPL or the well proven OTL library.
procedure TForm1.StopClick(Sender: TObject);
var
i : Integer;
aThread : TThread;
begin
// Kill the worker threads by pushing nil
for i := 1 to cThreadCount do
aQueue.PushItem(nil);
// Since the worker threads synchronizes with the main thread,
// we must wait for them in another thread.
aThread := TThread.CreateAnonymousThread(
procedure
begin
aCounter.WaitFor; // Wait for threads to finish
aCounter.Free;
aQueue.Free;
end
);
aThread.FreeOnTerminate := false;
aThread.Start;
aThread.WaitFor; // Safe to wait for the anonymous thread
aThread.Free;
end;
Terminate only sets the Terminated property to true. It's important that the internal loop of the thread checks the Terminated property periodically, and returns from the Execute method when it is set to true. After that, use WaitFor in the main thread to check the threads have all ended before you free queue or thread-pool objects.
I created a class for writing thread-safe log in a text file using CriticalSection.
I am not an expert of CriticalSection and multi-threading programming (...and Delphi), I'm definitely doing something wrong...
unit ErrorLog;
interface
uses
Winapi.Windows, System.SysUtils;
type
TErrorLog = class
private
FTextFile : TextFile;
FLock : TRTLCriticalSection;
public
constructor Create(const aLogFilename:string);
destructor Destroy; override;
procedure Write(const ErrorText: string);
end;
implementation
constructor TErrorLog.Create(const aLogFilename:string);
begin
inherited Create;
InitializeCriticalSection(FLock);
AssignFile(FTextFile, aLogFilename);
if FileExists(aLogFilename) then
Append(FTextFile)
else
Rewrite(FTextFile);
end;
destructor TErrorLog.Destroy;
const
fmTextOpenWrite = 55218;
begin
EnterCriticalSection(FLock);
try
if TTextRec(FTextFile).Mode <> fmTextOpenWrite then
CloseFile(FTextFile);
inherited Destroy;
finally
LeaveCriticalSection(FLock);
DeleteCriticalSection(FLock);
end;
end;
procedure TErrorLog.Write(const ErrorText: string);
begin
EnterCriticalSection(FLock);
try
WriteLn(FTextFile, ErrorText);
finally
LeaveCriticalSection(FLock);
end;
end;
end.
to test the class I created a form with a timer set to 100 milliseconds:
procedure TForm1.Timer1Timer(Sender: TObject);
var
I : integer;
aErrorLog : TErrorLog;
begin
aErrorLog := nil;
for I := 0 to 1000 do begin
try
aErrorLog := TErrorLog.Create(FormatDateTime('ddmmyyyy', Now) + '.txt');
aErrorLog.Write('new line');
finally
if Assigned(aErrorLog) then FreeAndNil(aErrorLog);
end;
end;
end;
the logs are written, but occasionally raise I/O Error 32 exception on CloseFile(FTextFile) (probably because in use in another thread)
where am I doing wrong?
UPDATE:
after reading all the comments and the answers I have totally changed approach. I share my solution.
ThreadUtilities.pas
(* Implemented for Delphi3000.com Articles, 11/01/2004
Chris Baldwin
Director & Chief Architect
Alive Technology Limited
http://www.alivetechnology.com
*)
unit ThreadUtilities;
interface
uses Windows, SysUtils, Classes;
type
EThreadStackFinalized = class(Exception);
TSimpleThread = class;
// Thread Safe Pointer Queue
TThreadQueue = class
private
FFinalized: Boolean;
FIOQueue: THandle;
public
constructor Create;
destructor Destroy; override;
procedure Finalize;
procedure Push(Data: Pointer);
function Pop(var Data: Pointer): Boolean;
property Finalized: Boolean read FFinalized;
end;
TThreadExecuteEvent = procedure (Thread: TThread) of object;
TSimpleThread = class(TThread)
private
FExecuteEvent: TThreadExecuteEvent;
protected
procedure Execute(); override;
public
constructor Create(CreateSuspended: Boolean; ExecuteEvent: TThreadExecuteEvent; AFreeOnTerminate: Boolean);
end;
TThreadPoolEvent = procedure (Data: Pointer; AThread: TThread) of Object;
TThreadPool = class(TObject)
private
FThreads: TList;
FThreadQueue: TThreadQueue;
FHandlePoolEvent: TThreadPoolEvent;
procedure DoHandleThreadExecute(Thread: TThread);
public
constructor Create( HandlePoolEvent: TThreadPoolEvent; MaxThreads: Integer = 1); virtual;
destructor Destroy; override;
procedure Add(const Data: Pointer);
end;
implementation
{ TThreadQueue }
constructor TThreadQueue.Create;
begin
//-- Create IO Completion Queue
FIOQueue := CreateIOCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);
FFinalized := False;
end;
destructor TThreadQueue.Destroy;
begin
//-- Destroy Completion Queue
if (FIOQueue <> 0) then
CloseHandle(FIOQueue);
inherited;
end;
procedure TThreadQueue.Finalize;
begin
//-- Post a finialize pointer on to the queue
PostQueuedCompletionStatus(FIOQueue, 0, 0, Pointer($FFFFFFFF));
FFinalized := True;
end;
(* Pop will return false if the queue is completed *)
function TThreadQueue.Pop(var Data: Pointer): Boolean;
var
A: Cardinal;
OL: POverLapped;
begin
Result := True;
if (not FFinalized) then
//-- Remove/Pop the first pointer from the queue or wait
GetQueuedCompletionStatus(FIOQueue, A, ULONG_PTR(Data), OL, INFINITE);
//-- Check if we have finalized the queue for completion
if FFinalized or (OL = Pointer($FFFFFFFF)) then begin
Data := nil;
Result := False;
Finalize;
end;
end;
procedure TThreadQueue.Push(Data: Pointer);
begin
if FFinalized then
Raise EThreadStackFinalized.Create('Stack is finalized');
//-- Add/Push a pointer on to the end of the queue
PostQueuedCompletionStatus(FIOQueue, 0, Cardinal(Data), nil);
end;
{ TSimpleThread }
constructor TSimpleThread.Create(CreateSuspended: Boolean;
ExecuteEvent: TThreadExecuteEvent; AFreeOnTerminate: Boolean);
begin
FreeOnTerminate := AFreeOnTerminate;
FExecuteEvent := ExecuteEvent;
inherited Create(CreateSuspended);
end;
procedure TSimpleThread.Execute;
begin
if Assigned(FExecuteEvent) then
FExecuteEvent(Self);
end;
{ TThreadPool }
procedure TThreadPool.Add(const Data: Pointer);
begin
FThreadQueue.Push(Data);
end;
constructor TThreadPool.Create(HandlePoolEvent: TThreadPoolEvent;
MaxThreads: Integer);
begin
FHandlePoolEvent := HandlePoolEvent;
FThreadQueue := TThreadQueue.Create;
FThreads := TList.Create;
while FThreads.Count < MaxThreads do
FThreads.Add(TSimpleThread.Create(False, DoHandleThreadExecute, False));
end;
destructor TThreadPool.Destroy;
var
t: Integer;
begin
FThreadQueue.Finalize;
for t := 0 to FThreads.Count-1 do
TThread(FThreads[t]).Terminate;
while (FThreads.Count > 0) do begin
TThread(FThreads[0]).WaitFor;
TThread(FThreads[0]).Free;
FThreads.Delete(0);
end;
FThreadQueue.Free;
FThreads.Free;
inherited;
end;
procedure TThreadPool.DoHandleThreadExecute(Thread: TThread);
var
Data: Pointer;
begin
while FThreadQueue.Pop(Data) and (not TSimpleThread(Thread).Terminated) do begin
try
FHandlePoolEvent(Data, Thread);
except
end;
end;
end;
end.
ThreadFileLog.pas
(* From: http://delphi.cjcsoft.net/viewthread.php?tid=45763 *)
unit ThreadFileLog;
interface
uses Windows, ThreadUtilities, System.Classes;
type
PLogRequest = ^TLogRequest;
TLogRequest = record
LogText : String;
FileName : String;
end;
TThreadFileLog = class(TObject)
private
FThreadPool: TThreadPool;
procedure HandleLogRequest(Data: Pointer; AThread: TThread);
public
constructor Create();
destructor Destroy; override;
procedure Log(const FileName, LogText: string);
end;
implementation
uses
System.SysUtils;
(* Simple reuse of a logtofile function for example *)
procedure LogToFile(const FileName, LogString: String);
var
F: TextFile;
begin
AssignFile(F, FileName);
if not FileExists(FileName) then
Rewrite(F)
else
Append(F);
try
Writeln(F, LogString);
finally
CloseFile(F);
end;
end;
constructor TThreadFileLog.Create();
begin
FThreadPool := TThreadPool.Create(HandleLogRequest, 1);
end;
destructor TThreadFileLog.Destroy;
begin
FThreadPool.Free;
inherited;
end;
procedure TThreadFileLog.HandleLogRequest(Data: Pointer; AThread: TThread);
var
Request: PLogRequest;
begin
Request := Data;
try
LogToFile(Request^.FileName, Request^.LogText);
finally
Dispose(Request);
end;
end;
procedure TThreadFileLog.Log(const FileName, LogText: string);
var
Request: PLogRequest;
begin
New(Request);
Request^.LogText := LogText;
Request^.FileName := FileName;
FThreadPool.Add(Request);
end;
end.
Basic form example
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls,
Vcl.StdCtrls, ThreadFileLog;
type
TForm1 = class(TForm)
BtnStart: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure BtnStartClick(Sender: TObject);
private
FThreadFileLog : TThreadFileLog;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.BtnStartClick(Sender: TObject);
var
I : integer;
aNow : TDateTime;
begin
aNow := Now;
for I := 0 to 500 do
FThreadFileLog.Log(
FormatDateTime('ddmmyyyyhhnn', aNow) + '.txt',
FormatDateTime('dd-mm-yyyy hh:nn:ss.zzz', aNow) + ': I: ' + I.ToString
);
ShowMessage('logs are performed!');
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FThreadFileLog := TThreadFileLog.Create();
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FThreadFileLog.Free;
ReportMemoryLeaksOnShutdown := true;
end;
end.
Output log:
30-11-2014 14.01.13.252: I: 0
30-11-2014 14.01.13.252: I: 1
30-11-2014 14.01.13.252: I: 2
30-11-2014 14.01.13.252: I: 3
30-11-2014 14.01.13.252: I: 4
30-11-2014 14.01.13.252: I: 5
30-11-2014 14.01.13.252: I: 6
30-11-2014 14.01.13.252: I: 7
30-11-2014 14.01.13.252: I: 8
30-11-2014 14.01.13.252: I: 9
...
30-11-2014 14.01.13.252: I: 500
Instead of checking TTextRec(FTextFile).Mode <> fmTextOpenWrite you should check whether your file is closed or not, and if it is not closed then you close it.
Try replacing the mentioned check with this code:
if TTextRec(FTextFile).Mode <> fmClosed then
CloseFile(FTextFile);
Edited:
This has nothing to do with antivirus locking the file. This is just a simple mistake in the destructor.
File is already opened in open write mode, original code is closing the file only when it is not in open write mode - so it is never closing the file.
Hope this explains where the mistake has happened.
As for the overall design of the logger's class. This was not the question, questions was simple, and I've provided a simple and working solution.
I think that if Simone would want us to teach him how to design logger class then he would ask for it.
If you want an error log class, where multiple threads can write to a log file, it is correct to protect the writing method with a critical section.
Now, since you will only instantiate one of those error logging objects in your application, there is no need to protect the destructor method with a critical section.
The location of your error log file should reside in the application data folder.
The I/O error 32 is: The process cannot access the file because it is being used by another process.
The reason for this sharing violation could be in your application or an external application.
Writing inside the application directory could trigger some antivirus protection for example. Or your application is holding the file open in several places with different file modes.
Your test is flawed in multiple ways:
Instantiate the error log class once at application start, and destroy it when the application closes.
Write to your error log from different threads, not from multiple iterations within a timer event.
A timer event should only execute a program sequence for a short duration.
A try / finally sequence is structured like this:
anObject := TObject.Create;
try
// Do something with anObject
finally
anObject.Free;
end;
Original Question
In our Delphi XE4 application we use a TOmniEventMonitor to receive messages from other tasks. As long as this is running in the main thread, it works fine, but once I put the same code in a task, the TOmniEventMonitor stops receiving messages. I have included a simple example of this below -- clicking Button_TestInMainThread results in a file being written as expected, clicking Button_TestInBackgroundThread does not. Is this by design, or is there some way to get this working while still using TOmniEventMonitor?
unit mainform;
interface
uses
Winapi.Windows, Winapi.Messages,
System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics,Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
OtlTask, OtlTaskControl, OtlComm, OtlEventMonitor;
const
MY_OMNI_MESSAGE = 134;
type
TOmniEventMonitorTester = class(TObject)
fName : string;
fOmniEventMonitor : TOmniEventMonitor;
fOmniTaskControl : IOmniTaskControl;
constructor Create(AName : string);
destructor Destroy(); override;
procedure HandleOmniTaskMessage(const task: IOmniTaskControl; const msg: TOmniMessage);
end;
TTestLauncherTask = class(TOmniWorker)
fOmniTaskMonitorTester : TOmniEventMonitorTester;
function Initialize() : boolean; override;
end;
TForm1 = class(TForm)
Button_TestInMainThread: TButton;
Button_TestInBackgroundThread: TButton;
procedure Button_TestInMainThreadClick(Sender: TObject);
procedure Button_TestInBackgroundThreadClick(Sender: TObject);
private
fOmniEventMonitorTester : TOmniEventMonitorTester;
fTestLauncherTask : IOmniTaskControl;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure OmniTaskProcedure_OneShotTimer(const task: IOmniTask);
begin
Sleep(1000);
task.Comm.Send(MY_OMNI_MESSAGE);
end;
constructor TOmniEventMonitorTester.Create(AName : string);
begin
inherited Create();
fName := AName;
fOmniEventMonitor := TOmniEventMonitor.Create(nil);
fOmniEventMonitor.OnTaskMessage := HandleOmniTaskMessage;
fOmniTaskControl := fOmniEventMonitor.Monitor(CreateTask(OmniTaskProcedure_OneShotTimer)).Run();
end;
destructor TOmniEventMonitorTester.Destroy();
begin
fOmniEventMonitor.Free();
inherited Destroy();
end;
procedure TOmniEventMonitorTester.HandleOmniTaskMessage(const task: IOmniTaskControl; const msg: TOmniMessage);
var
Filename : string;
F : TextFile;
begin
Filename := IncludeTrailingPathDelimiter(ExtractFileDir(ParamStr(0))) + fName + '.txt';
AssignFile(F, Filename);
Rewrite(F);
Writeln(F, fName);
CloseFile(F);
end;
function TTestLauncherTask.Initialize() : boolean;
begin
result := inherited Initialize();
if result then begin
fOmniTaskMonitorTester := TOmniEventMonitorTester.Create('background');
end;
end;
procedure TForm1.Button_TestInMainThreadClick(Sender: TObject);
begin
fOmniEventMonitorTester := TOmniEventMonitorTester.Create('main');
end;
procedure TForm1.Button_TestInBackgroundThreadClick(Sender: TObject);
begin
fTestLauncherTask := CreateTask(TTestLauncherTask.Create()).Run();
end;
end.
Additional Observations
With the following code it seems to be possible to successfully use a TOmniEventMonitor within a background thread. This really is a very clumsy solution -- an IOmniTwoWayChannel gets created but not used in any meaningful way -- but as soon as I try to simplify the code by commenting out either of the lines marked "don't remove!", HandleTaskMessage doesn't get called any more. Can anybody tell me what I am doing wrong here?
unit mainform;
interface
uses
Winapi.Windows, Winapi.Messages,
System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
DSiWin32, GpLists, OtlTask, OtlTaskControl, OtlCommon, OtlComm, OtlEventMonitor;
const
MY_OMNI_MESSAGE = 134;
type
TOmniEventMonitorTestTask = class(TOmniWorker)
fOmniTaskControl : IOmniTaskControl;
fOmniTwoWayChannel : IOmniTwoWayChannel;
fOmniEventMonitor : TOmniEventMonitor;
function Initialize() : boolean; override;
procedure HandleTaskMessage(const task: IOmniTaskControl; const msg: TOmniMessage);
procedure HandleTaskTerminated(const task: IOmniTaskControl);
end;
TForm1 = class(TForm)
Button_TestInBackgroundThread: TButton;
procedure Button_TestInBackgroundThreadClick(Sender: TObject);
private
fTestTask : IOmniTaskControl;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure OmniTaskProcedure_OneShotTimer(const task: IOmniTask);
begin
Sleep(1000);
task.Comm.Send(MY_OMNI_MESSAGE); // don't remove!
(task.Param['Comm'].AsInterface as IOmniCommunicationEndpoint).Send(MY_OMNI_MESSAGE);
end;
procedure TOmniEventMonitorTestTask.HandleTaskMessage(const task: IOmniTaskControl; const msg: TOmniMessage);
var
Filename : string;
F : TextFile;
begin
Filename := IncludeTrailingPathDelimiter(ExtractFileDir(ParamStr(0))) + 'HandleTaskMessage.txt';
AssignFile(F, Filename);
Rewrite(F);
Writeln(F, 'HandleTaskMessage!');
CloseFile(F);
end;
procedure TOmniEventMonitorTestTask.HandleTaskTerminated(const task: IOmniTaskControl);
var
Filename : string;
F : TextFile;
begin
Filename := IncludeTrailingPathDelimiter(ExtractFileDir(ParamStr(0))) + 'HandleTaskTerminated.txt';
AssignFile(F, Filename);
Rewrite(F);
Writeln(F, 'HandleTaskTerminated!');
CloseFile(F);
end;
function TOmniEventMonitorTestTask.Initialize() : boolean;
begin
result := inherited Initialize();
if result then begin
fOmniEventMonitor := TOmniEventMonitor.Create(nil);
fOmniEventMonitor.OnTaskMessage := HandleTaskMessage;
fOmniEventMonitor.OnTaskTerminated := HandleTaskTerminated;
fOmniTwoWayChannel := CreateTwoWayChannel();
Task.RegisterComm(fOmniTwoWayChannel.Endpoint1); // don't remove!
fOmniTaskControl := fOmniEventMonitor.Monitor( CreateTask(OmniTaskProcedure_OneShotTimer) ).SetParameter('Comm', fOmniTwoWayChannel.Endpoint2).Run();
end;
end;
procedure TForm1.Button_TestInBackgroundThreadClick(Sender: TObject);
begin
fTestTask := CreateTask(TOmniEventMonitorTestTask.Create()).Run();
end;
end.
There is no problem with TOmniEventMonitor running inside of a thread, provided there is a message pump handling the messages for it. I put this block of code together to demonstrate. This works as expected.
procedure TMyThread.Execute;
var
Message: TMsg;
begin
FreeOnTerminate := True;
fOmniEventMonitor := TOmniEventMonitor.Create(nil);
fOmniEventMonitor.OnTaskMessage := HandleOmniTaskMessage;
fOmniTaskControl := fOmniEventMonitor.Monitor(CreateTask(OmniTaskProcedure_OneShotTimer)).Run();
try
while not Terminated do
begin
if MsgWaitForMultipleObjects(0, nil^, False, 1000, QS_ALLINPUT) = WAIT_OBJECT_0 then
begin
while PeekMessage(Message, 0, 0, 0, PM_REMOVE) do
begin
TranslateMessage(Message);
DispatchMessage(Message);
end;
end;
end;
finally
fOmniTaskControl := nil;
fOmniEventMonitor.Free;
end;
end;
From what I can see, the TOmniTaskExecutor waits for messages to specific handles. In your code example, it's the terminate event and a couple of comm handles. The messages for the TOmniEventMonitor are never processed.
Changing your TTestLauncherTask.Initialize to the following results in it correctly writing out the file. DoNothingProc is just an empty method on the class.
function TTestLauncherTask.Initialize() : boolean;
begin
result := inherited Initialize();
if result then begin
fOmniTaskMonitorTester := TOmniEventMonitorTester.Create('background');
// Tell the task about the event monitor
Task.RegisterWaitObject(fOmniTaskMonitorTester.fOmniEventMonitor.MessageWindow, DoNothingProc);
end;
end;
I am adding the message window for the TOmniEventMonitor to the Task WaitObject list so the handle is then registered with the MsgWaitForMultipleObjectsEx call and waiting for Remi and David to tear my message handling to shreds :)
This is my second question about this, im having some troubles with this >.<
Well, I just want to create a limited number of threads (in this case, I want 10 threads), and then each thread will pick up a name in my list and get some data in my site.
My system works pretty well, but my multi thread system still fails =(
--
I tried the code posted by LU RD, but the main thread don't wait the threads finish the queue, and just stops =(
The code:
uses
Classes,SyncObjs,Generics.Collections;
Type
TMyConsumerItem = class(TThread)
private
FQueue : TThreadedQueue<TProc>;
FSignal : TCountDownEvent;
protected
procedure Execute; override;
public
constructor Create( aQueue : TThreadedQueue<TProc>; aSignal : TCountdownEvent);
end;
constructor TMyConsumerItem.Create(aQueue: TThreadedQueue<TProc>; aSignal : TCountDownEvent);
begin
Inherited Create(false);
Self.FreeOnTerminate := true;
FQueue := aQueue;
FSignal := aSignal;
end;
procedure TMyConsumerItem.Execute;
var
aProc : TProc;
begin
try
repeat
FQueue.PopItem(aProc);
if not Assigned(aProc) then
break; // Drop this thread
aProc();
until Terminated;
finally
FSignal.Signal;
end;
end;
procedure DoSomeJob(myListItems : TStringList);
const
cThreadCount = 10;
cMyQueueDepth = 100;
var
i : Integer;
aQueue : TThreadedQueue<TProc>;
aCounter : TCountDownEvent;
function CaptureJob( const aString : string) : TProc;
begin
Result :=
procedure
begin
// Do some job with aString
end;
end;
begin
aQueue := TThreadedQueue<TProc>.Create(cMyQueueDepth);
aCounter := TCountDownEvent.Create(cThreadCount);
try
for i := 1 to cThreadCount do
TMyConsumerItem.Create(aQueue,aCounter);
for i := 0 to myListItems.Count-1 do begin
aQueue.PushItem( CaptureJob( myListItems[i]));
end;
finally
for i := 1 to cThreadCount do
aQueue.PushItem(nil);
aCounter.WaitFor; // Wait for threads to finish
aCounter.Free;
aQueue.Free;
end;
end;
My other question: Multi Thread Delphi
Im using Delphi XE3.
First, if you want to call the procedure DoSomeJob() and block until ready from the main thread, there is a caveat. If your worker threads are synchronizing with the main thread, there is a dead-lock situation with aCounter.WaitFor and TThread.Synchronize().
I am assuming that this is what is happening to you, guessing here.
There is a way to handle that as I will show in this answer.
Second, normally the worker threads should be handled by a thread pool, to avoid create/destroy threads all the time. Pass your job to the thread pool, so everything is run and waited for inside a thread. This avoids blocking the main thread.
I will leave this up to you. Once that framework is written, threading will be easier. If this seems complex, try OTL threading framework instead.
Here is an example where the main thread can wait for DoSomeJob() in a safe manner.
An anonymous thread is created to wait for the aCounter to signal.
This example uses a TMemo and a TButton. Just create a form with these components and connect the button OnClick event to the ButtonClick method.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
procedure DoSomeJob( myListItems : TStringList);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
SyncObjs, Generics.Collections;
{- Include TMyConsumerItem class here }
procedure TForm1.Button1Click(Sender: TObject);
var
aList : TStringList;
i : Integer;
begin
aList := TStringList.Create;
Screen.Cursor := crHourGlass;
try
for i := 1 to 20 do aList.Add(IntToStr(i));
DoSomeJob(aList);
finally
aList.Free;
Screen.Cursor := crDefault;
end;
end;
procedure TForm1.DoSomeJob(myListItems: TStringList);
const
cThreadCount = 10;
cMyQueueDepth = 100;
var
i: Integer;
aQueue: TThreadedQueue<TProc>;
aCounter: TCountDownEvent;
function CaptureJob(const aString: string): TProc;
begin
Result :=
procedure
var
i,j : Integer;
begin
// Do some job with aString
for i := 0 to 1000000 do
j := i;
// Report status to main thread
TThread.Synchronize(nil,
procedure
begin
Memo1.Lines.Add('Job with:'+aString+' done.');
end
);
end;
end;
var
aThread : TThread;
begin
aQueue := TThreadedQueue<TProc>.Create(cMyQueueDepth);
aCounter := TCountDownEvent.Create(cThreadCount);
try
for i := 1 to cThreadCount do
TMyConsumerItem.Create(aQueue, aCounter);
for i := 0 to myListItems.Count - 1 do
begin
aQueue.PushItem(CaptureJob(myListItems[i]));
end;
// Kill the worker threads
for i := 1 to cThreadCount do
aQueue.PushItem(nil);
finally
// Since the worker threads synchronizes with the main thread,
// we must wait for them in another thread.
aThread := TThread.CreateAnonymousThread(
procedure
begin
aCounter.WaitFor; // Wait for threads to finish
aCounter.Free;
aQueue.Free;
end
);
aThread.FreeOnTerminate := false;
aThread.Start;
aThread.WaitFor; // Safe to wait for the anonymous thread
aThread.Free;
end;
end;
end.