I have an application that is 100% Delphi code. It is a 64 bit windows console application, with a workload manager, and a fixed number of workers. This is done via creation of threads and each thread is a worker. The thread does not die, it pulls works from its own queue that the workload manager populates.
This appears to work just fine.
What I am finding, however, is that on a 16 core system I am seeing processing times around 90 minutes (it has 2,000,000+ workloads; and each does database work). When I added 16 to 32 cores, I saw the performance drop! There is no database contention. Essentially, the DB is waiting for things to do.
Each thread has its own DB connection. Each thread's queries use only that threads connection.
I updated the Delphi MM to use ScaleMM2; which made a big improvement; but I am still at a loss as to why increasing cores reduces performance.
When app has 256 threads, on 32 cores, CPU total use at 80%.
When app has 256 threads, on 16 cores, CPU total use at 100% (which is why I wanted to add cores) -- and it got slower :-(
I have applied as much as the advice as I can understand to the code-base.
ie - Functions not returning strings, using Const for arguments, protecting "shared" data with small critical sections (actually using Multi-read Exclusive Write). I currently do not assign processor affinity; I was reading conflicting advice on using it .. so I am currently not (would be trival to add, just not there today).
Questions - slanted towards I "think" the issue is around thread contention ...
How do I find confirm thread-contention is the issue? Are there tools available specifically for this type of contention identification?
How can I determine what is using "heap" and what is not, to further reduce contention there?
Insights, guidance, pointers would be appreciated.
Can provide relevant code areas ... if I knew what was relevant.
Procedure TXETaskWorkloadExecuterThread.Enqueue(Const Workload: TXETaskWorkload);
Begin
// protect your own queue
FWorkloadQueue.Enter;
FWorkloads.Add(Workload);
FWorkloadQueue.Leave;
End;
Procedure TXETaskManager.Enqueue(Const Workload: TXETaskWorkload);
Begin
If FWorkloadCount >= FMaxQueueSize Then Begin
WaitForEmptyQueue;
FWorkloadCount := 0;
End;
FExecuters[FNextThread].Enqueue(Workload);
// round-robin the queue
Inc(FNextThread);
Inc(FWorkloadCount);
If FNextThread >= FWorkerThreads Then Begin
FNextThread := 0;
End;
End;
Function TXETaskWorkloadExecuterThread.Dequeue(Var Workload: TXETaskWorkload): Boolean;
Begin
Workload := Nil;
Result := False;
FWorkloadQueue.Enter;
Try
If FNextWorkload < FWorkloads.Count Then Begin
Workload := FWorkloads[FNextWorkload];
Inc(FNextWorkload);
If Workload Is TXETaskWorkLoadSynchronize Then Begin
FreeAndNil(Workload);
Exit;
End;
Result := True;
End Else Begin
FWorkloads.Clear;
FNextWorkload := 0;
FHaveWorkloadInQueue.ResetEvent;
FEmptyAndFinishedQueue.SetEvent;
End;
Finally
FWorkloadQueue.Leave;
End;
End;
EDIT ---
Thanks for all the comments. Clarifications.
This system/VM has nothing else on it. The executable in question is the only thing using the CPU. Single threaded performance means linear. I have simply made this a divide/conquer. If I have 5,000,000 cars to park, and I have 30 drivers with 30 different parking lots. I can tell each driver to wait for the other drive to finish parking, it will be slower than telling 30 drivers to concurrently park cars.
Profiling in single threaded shows there is nothing that is causing this. I have seen mention on this board about Delphi and multi-core performance "gotcha's" (mostly related to string handling and LOCK).
The DB essentially is saying that it is bored, and waiting for things to do. I have checked with a copy of Intels vTune. Generally speaking, it says ... locks. But, I cannot find out where. What I have is pretty simple to my mind, and the current areas for locks are necessary and small. What I cannot see is locks that might be happening due to other things .. like strings creating a lock, or thread 1 causing some issue on the main process via accessing that data (even though protected via a critical section).
Continuing to research. Thanks again for the feedback/ideas.
Your Workload Manager is deciding which thread gets which work item. If a given thread blocks (say the work is long, DB latency, etc), you are queuing more items to that thread even though they might not get processed for awhile, if at all.
Typically, work items should be stored in a single shared queue that multiple threads then pull from. When any given thread is ready, it pulls the next available work item. For example:
constructor TXETaskManager.Create;
var
I: Integer;
begin
FWorkloadQueue := TCriticalSection.Create;
FWorkloads := TList<TXETaskWorkload>.Create;
FEmptyQueue := TEvent.Create(nil, True, True, '');
FHaveWorkloadInQueue := TEvent.Create(nil, True, False, '');
FNotFullQueue := TEvent.Create(nil, True, True, '');
FTermEvent := TEvent.Create(nil, True, False, '');
...
FMaxQueueSize := ...;
FWorkerThreads := ...;
for I := 0 to FWorkerThreads-1 do
FExecuters[I] := TXETaskWorkloadExecuterThread.Create(Self);
end;
destructor TXETaskManager.Destroy;
begin
for I := 0 to FWorkerThreads-1 do
FExecuters[I].Terminate;
FTermEvent.SetEvent;
for I := 0 to FWorkerThreads-1 do
begin
FExecuters[I].WaitFor;
FExecuters[I].Free;
end;
FWorkloadQueue.Free;
FWorkloads.Free;
FEmptyQueue.Free;
FHaveWorkloadInQueue.Free;
FNotFullQueue.Free;
FTermEvent.Free;
...
inherited;
end;
procedure TXETaskManager.Enqueue(Const Workload: TXETaskWorkload);
begin
FWorkloadQueue.Enter;
try
while FWorkloads.Count >= FMaxQueueSize do
begin
FWorkloadQueue.Leave;
FNotFullQueue.WaitFor(INFINITE);
FWorkloadQueue.Enter;
end;
FWorkloads.Add(Workload);
if FWorkloads.Count = 1 then
begin
FEmptyQueue.ResetEvent;
FHaveWorkloadInQueue.SetEvent;
end;
if FWorkloads.Count >= FMaxQueueSize then
FNotFullQueue.ResetEvent;
finally
FWorkloadQueue.Leave;
end;
end;
function TXETaskManager.Dequeue(var Workload: TXETaskWorkload): Boolean;
begin
Result := False;
Workload := nil;
FWorkloadQueue.Enter;
try
if FWorkloads.Count > 0 then
begin
Workload := FWorkloads[0];
FWorkloads.Delete(0);
Result := True;
if FWorkloads.Count = (FMaxQueueSize-1) then
FNotFullQueue.SetEvent;
if FWorkloads.Count = 0 then
begin
FHaveWorkloadInQueue.ResetEvent;
FEmptyQueue.SetEvent;
end;
end;
finally
FWorkloadQueue.Leave;
end;
end;
constructor TXETaskWorkloadExecuterThread.Create(ATaskManager: TXETaskManager);
begin
inherited Create(False);
FTaskManager := ATaskManager;
end;
procedure TXETaskWorkloadExecuterThread.Execute;
var
Arr: THandleObjectArray;
Event: THandleObject;
Workload: TXETaskWorkload;
begin
SetLength(Arr, 2);
Arr[0] := FTaskManager.FHaveWorkloadInQueue;
Arr[1] := FTaskManager.FTermEvent;
while not Terminated do
begin
case TEvent.WaitForMultiple(Arr, INFINITE, False, Event) of
wrSignaled:
begin
if Event = FTaskManager.FHaveWorkloadInQueue then
begin
if FTaskManager.Dequeue(Workload) then
try
// process Workload as needed...
finally
Workload.Free;
end;
end;
end;
wrError: begin
RaiseLastOSError;
end;
end;
end;
end;
If you find threads are not getting enough work, you can adjust your thread count as needed. You typically shouldn't be using very many more threads than you have CPU cores available.
Related
After struggling to avoid deadlocks using Synchronize() with DirectShow I decided to switch to an optimistic lock pattern. This is my first time using AtomicCmpExchange() and I found very few Delphi examples online. I don't see any downside and don't know why more thread locking isn't done this way.
What are the pitfalls of the following code? The lack of examples make me concerned that there is a fundamental flaw with this approach.
procedure TCueRunner.Lock(Desc: String);
begin
// cs.Enter;
var StopWatch := TStopwatch.StartNew;
while (AtomicCmpExchange(LockAtomic, 1, 0) = 1) do
begin
Sleep(1);
if StopWatch.ElapsedMilliseconds > MUTEX_TIMEOUT then
begin
LockAtomic := 0;
LogMsg('CRITICAL! TCueRunner lock timed out in function '+Desc);
break;
end;
end;
end;
procedure TCueRunner.Unlock;
begin
LockAtomic := 0; // This should be an atomic operation
// cs.Leave;
end;
First, your lock cannot be recursively used. It's not always an issue, but it's usually worth it to make it work recursively as it's pain to always make sure it's not used in this fashion.
Second, as commented by Dalija, it's not really an expected behavior that a lock attempt that timeout would cause the lock to be automatically unlocked.
Third, if locking fails, your method should raise an exception, otherwise, the calling code will proceed as if the lock had succeeded. Alternately, you could change your method to a function returning a boolean indicating if the lock succeeded or not.
If I adapt your code, that would give something like this (Untested) :
procedure TCueRunner.Lock(Desc: String);
begin
var StopWatch := TStopwatch.StartNew;
repeat
var PrevLockValue := AtomicCmpExchange(LockAtomic, TThread.CurrentThread.ThreadID, 0);
if PrevLockValue = 0 then //wasn't locked
FLockCount := 1
else if PrevLockValue = TThread.CurrentThread.ThreadID then //Was locked previously, but by the current thread.
Inc(FLockCount)
else
begin //Is locked by another thread
Sleep(1);
if StopWatch.ElapsedMilliseconds > MUTEX_TIMEOUT then
raise Exception.Create('CRITICAL! TCueRunner lock timed out in function '+Desc);
CONTINUE;
end;
BREAK;
until False;
end;
procedure TCueRunner.Unlock;
begin
if LockAtomic = TThread.CurrentThread.ThreadID then
begin
Dec(FLockCount);
if FLockCount = 0 then
LockAtomic := 0;
end else
LogMsg('Unlocking from the wrong thread!!!!');//Or not locked...
end;
Note : This should work properly on WIN32/64, I'm unsure about other platforms.
I have the following code running on Windows 10.
function SingleProcessorMask(const ProcessorIndex: Integer): DWORD_PTR;
begin
Result:= 1; Result:= Result shl (ProcessorIndex); //Make sure it works on processor 33 and up.
end;
procedure TForm2.BtnCreateLookup5x5to3x3UsingSpeculativeExplorationClick(Sender: TObject);
var
ThreadCount: integer;
Threads: TArray<TThread>;
CurrentProcessor: integer;
i,a: integer;
Done: boolean;
begin
ThreadCount:= System.CpuCount;
SetLength(Threads, ThreadCount);
CurrentProcessor:= GetCurrentProcessorNumber;
a:= 0;
for i:= 1 to ThreadCount-1 do begin
Threads[i]:= TThread.CreateAnonymousThread(procedure begin
CreateLookupUsingGridSolver(i, ThreadCount);
end);
Threads[i].FreeOnTerminate:= false;
if (CurrentProcessor = a) then Inc(a); //Skip the current processor.
Inc(a);
//if (SetThreadAffinityMask(Threads[i].handle, SingleProcessorMask(a))) = 0 then RaiseLastOSError; << fails here as well.
Threads[i].Start;
if (SetThreadAffinityMask(Threads[i].handle, SingleProcessorMask(a))) = 0 then RaiseLastOSError;
end; {for i}
CreateLookupUsingGridSolver(0, ThreadCount, NewLookup);
{Wait for all threads to finish}
.....
//Rest of the proc omitted to save space.
end;
I keep getting error 87, Incorrect parameter.
I'm fairly sure the SingleProcessorMask is correct.
Is there perhaps an issue with the TThread.Handle?
It does not matter if I call this code running as Administrator, running on a laptop or running on a i9. The result is always the same.
And yes, I really do need to force the threads, otherwise they all bunch up on the same core.
UPDATE
Once I fix the process affinity to match the system affinity, there is no need to muck around with assigning each thread to a specific core. In that case the automatic handling works. This is done using:
GetProcessAffinityMask(GetCurrentProcess(), ProcessAffinityMask, SystemAffinityMask);
SetProcessAffinityMask(GetCurrentProcess(), SystemAffinityMask);
//Error checking omitted for brevity
It looks like you are trying to create a separate thread for every CPU other than the "current" CPU that is running your OnClick handler. But, you never use CPU 0 in your affinity masks, because you increment a too soon. But more importantly, a thread's affinity mask must be a subset of the process's affinity mask, which specifies the CPUs the process is allowed to run on:
A thread can only run on the processors its process can run on. Therefore, the thread affinity mask cannot specify a 1 bit for a processor when the process affinity mask specifies a 0 bit for that processor.
The process affinity mask is itself a subset of the system affinity mask, which specifies which CPUs are installed.
So, the likely cause of your error is that you are calculating thread affinity masks that the OS rejects as invalid for your process.
Try something more like this instead (note: this doesn't take CPU processor groups into account, if the OS has more than 64 CPUs installed):
procedure TForm2.BtnCreateLookup5x5to3x3UsingSpeculativeExplorationClick(Sender: TObject);
var
ThreadCount, MaxThreadCount: integer;
Threads: TArray<TThread>;
i, CurrentProcessor: integer;
ProcessAffinityMask, SystemAffinityMask, AllowedThreadMask, NewThreadMask: DWORD_PTR;
Thread: TThread;
...
begin
if not GetProcessAffinityMask(GetCurrentProcess(), ProcessAffinityMask, SystemAffinityMask) then RaiseLastOSError;
// optional: up the CPUs this process can run on, if needed...
{
if not SetProcessAffinityMask(GetCurrentProcess(), SystemAffinityMask) then RaiseLastOSError;
ProcessAffinityMask := SystemAffinityMask;
}
AllowedThreadMask := DWORD_PTR(-1) and ProcessAffinityMask;
CurrentProcessor := GetCurrentProcessorNumber;
ThreadCount := 0;
MaxThreadCount := System.CpuCount;
NewThreadMask := 1;
SetLength(Threads, MaxThreadCount);
try
for i := 0 to MaxThreadCount-1 do
begin
if (i <> CurrentProcessor) and //Skip the current processor.
((AllowedThreadMask and NewThreadMask) <> 0) then // is this CPU allowed?
begin
Thread := TThread.CreateAnonymousThread(
procedure
begin
CreateLookupUsingGridSolver(...);
end
);
try
Thread.FreeOnTerminate := false;
if not SetThreadAffinityMask(Thread.Handle, NewThreadMask) then RaiseLastOSError;
Thread.Start;
except
Thread.Free;
raise;
end;
Threads[ThreadCount] := Thread;
Inc(ThreadCount);
end;
NewThreadMask := NewThreadMask shl 1;
end;
CreateLookupUsingGridSolver(...);
// Wait for all threads to finish...
// ...
finally
for i := 0 to ThreadCount-1 do
Threads[i].Free;
end;
end;
There are two arguments to SetThreadAffinityMask, the thread handle and the mask. It's pretty clear from the code that the thread handle is valid. Which leaves the mask. The documentation clearly states the following:
If the thread affinity mask requests a processor that is not selected for the process affinity mask, the last error code is ERROR_INVALID_PARAMETER.
It is rather hard to see what else could explain the behaviour that you report.
Background: I need to perform checks whether a bunch of network drives or remote computers are available. Since each DirectoryExists() needs a lot of time until a potential timeout, I perform the checks in separate threads. It can happen, that an end-user closes the application while some of the checks are still running. Since DirectoryExists() blocks, I have no chance of using the classical while not Terminated approach.
procedure TMyThread.Execute;
begin
AExists := DirectoryExists(AFilepath);
end;
Question 1: Is it a problem that some threads are still running when the application quits? Will Windows simply tidy up after me and that's it? Inside the IDE I get notification of un-freed objects, but outside IDE it just appears to be peaceful.
Question 2: Is it possible to terminate such simple threads with TerminateThread or is this potentially harmful in THIS case?
Question 3: I usually take the results from the threads in OnTerminate() event and let the threads FreeOnTerminate afterwards. If I wanted to free them myself, when should I do it? Can I free a thread in its OnTerminate event or is this a tiny bit too early? How would a thread inform me that it is done if not with OnTerminate?
Is it a problem that some threads are still running when the application quits?
Possibly, yes. It depends on what your code does after DirectoryExists() exits. You might end up trying to access things that no longer exist.
Will Windows simply tidy up after me and that's it?
To ensure everything is cleaned up properly, you are responsible for terminating your own threads. When the main VCL thread is done running, it will call ExitProcess(), which will forcibly terminate any secondary threads that are still running, which will not allow them to clean up after themselves, or notify any loaded DLLs that they are being detached from the threads.
Is it possible to terminate such simple threads with TerminateThread or is this potentially harmful in THIS case?
TerminateThread() is ALWAYS potentially harmful. NEVER use it.
I usually take the results from the Threads in OnTerminate() event and let the threads FreeOnTerminate afterwards.
That will not work if the main message loop has exited before the thread terminates. By default, the TThread.OnTerminate event is fired via a call to TThread.Synchronize(). Once the main message loop stops running, there won't be anything to process the pending Synchronize() requests, unless you run your own loop at app exit to call the RTL's CheckSynchronize() procedure until all of your threads have fully terminated.
if I wanted to free them myself, when should I do it?
Before your app wants to exit.
Can I free a thread in its OnTerminate event
No.
or is this a tiny bit too early?
That, and because it is always unsafe to free an object inside an event fired by that same object. The RTL still needs access to the object after the event handler exits.
That being said, since you don't have a clean way to terminate the threads safely, I suggest NOT allowing your app to exit when there are threads still running. When the user requests the app to exit, check if there are threads running, and if so then display a busy UI to the user, wait for all of the threads to terminate, and then exit the app.
For example:
constructor TMyThread.Create(...);
begin
inherited Create(False);
FreeOnTerminate := True;
...
end;
procedure TMyThread.Execute;
begin
...
if Terminated then Exit;
AExists := DirectoryExists(AFilepath);
if Terminated then Exit;
...
end;
type
TMainForm = class(TForm)
...
procedure FormClose(Sender: TObject; var Action: TCloseAction);
...
private
ThreadsRunning: Integer;
procedure StartAThread;
procedure ThreadTerminated(Sender: TObject);
...
end;
...
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if ThreadsRunning = 0 then Exit;
// signal threads to terminate themselves...
if CheckWin32Version(6) then
ShutdownBlockReasonCreate(Handle, 'Waiting for Threads to Terminate');
try
// display busy UI to user ...
repeat
case MsgWaitForMultipleObjects(1, System.Classes.SyncEvent, False, INFINITE, QS_ALLINPUT) of
WAIT_OBJECT_0 : CheckSynchronize;
WAIT_OBJECT_0+1 : Application.ProcessMessages;
WAIT_FAILED : RaiseLastOSError;
end;
until ThreadsRunning = 0;
// hide busy UI ...
finally
if CheckWin32Version(6) then
ShutdownBlockReasonDestroy(Handle);
end;
end;
procedure TMainForm.StartAThread;
var
Thread: TMyThread;
begin
Thread := TMyThread.Create(...);
Thread.OnTerminate := ThreadTerminated;
Thread.Start;
Inc(ThreadsRunning);
end;
procedure TMainForm.ThreadTerminated(Sender: TObject);
begin
Dec(ThreadsRunning);
...
end;
Alternatively:
type
TMainForm = class(TForm)
...
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
...
private
ThreadsRunning: Integer;
WaitingForClose: Boolean;
procedure StartAThread;
procedure ThreadTerminated(Sender: TObject);
...
end;
...
procedure TMainForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose := (ThreadsRunning = 0);
if CanClose or WaitingForClose then Exit;
// signal threads to terminate themselves...
WaitingForClose := True;
// display busy UI to user ...
if CheckWin32Version(6) then
ShutdownBlockReasonCreate(Handle, 'Waiting for Threads to Terminate');
end;
procedure TMainForm.StartAThread;
var
Thread: TMyThread;
begin
Thread := TMyThread.Create(...);
Thread.OnTerminate := ThreadTerminated;
Thread.Start;
Inc(ThreadsRunning);
end;
procedure TMainForm.ThreadTerminated(Sender: TObject);
begin
Dec(ThreadsRunning);
...
if WaitingForClose and (ThreadsRunning = 0) then
begin
WaitingForClose := False;
// hide busy UI ...
if CheckWin32Version(6) then
ShutdownBlockReasonDestroy(Handle);
Close;
end;
end;
Is it a problem that some threads are still running when the application quits?
When taken literally, this question is a little bit malformed. That is because after ExitProcess is called, which is how a Delphi application is ended by default, no threads are running.
The answer to the question "is it a problem that some threads didn't have a chance to finish" depends on what these threads failed to complete. You would have to carefully analyze thread code, but generally speaking this might be prone to errors.
Will Windows simply tidy up after me and that's it? Inside the IDE I get notification of un-freed objects, but outside IDE it just appears
to be peaceful.
The OS will reclaim allocated memory when the process address space is destroyed, all object handles will be closed when the process handle table is destroyed, entry points of all loaded libraries will be called with DLL_PROCESS_DETACH. I can't find any documentation on this but I also presume pending IO requests would be called to cancel.
But all of this does not mean there won't be any problems. Things can get messy, for instance, involving interprocess communications or synchronization objects. Documentation for ExitProcess details one such example: if a thread vanishes before releasing a lock that one of the libraries tries to acquire while detaching, there's a deadlock. This blog post gives another specific example where the exiting process is forcibly terminated by the OS if a thread attempts to enter a critical section that is orphaned by another already terminated thread.
While it may make sense to let go of resource releasing at exit time, particularly if cleanup is taking a considerable amount of time, it is possible to get it wrong for a non-trivial application. A robust strategy is to clean up everything before ExitProcess is called. OTOH if you find yourself in a situation where ExitProcess is already called, such as the process is detaching from your dll because of termination, the nearly only safe thing to do is to leave everything behind and return - every other dll could have already been unloaded and every other thread terminated.
Is it possible to terminate such simple threads with TerminateThread or is this potentially harmful in THIS case?
TerminateThread is advised to be used only in most extreme cases but since the question has a bold "THIS" what the code really does should be examined. Looking at the RTL code we can see that the worst that can happen is leaving a file handle open which is accessed for reading only. THIS is not a problem at process termination time since the handle will be closed shortly.
I usually take the results from the threads in OnTerminate() event and let the threads FreeOnTerminate afterwards. If I wanted to free
them myself, when should I do it?
The only strict rule is after they are finished executing. The choice would probably be guided by the design of the application. What would be different is, you wouldn't be able to use FreeOnTerminate and you would keep references to your threads to be able to free them. In the test case I worked on for answering this question, the worker threads which are finished are freed when a timer fires, kind of like a garbage collector.
Can I free a thread in its OnTerminate event or is this a tiny bit too early?
Freeing an object in one of its own event handlers induces a risk of operating on freed instance memory. The documentation specifically warns against this for components but in general this is applicable to all classes.
Even if you'd want to disregard the warning, this is a deadlock. Although the handler is called after Execute returns, OnTerminate is still synchronized from the ThreadProc. If you attempt to free the thread in the handler, it will cause a wait from the main thread for the thread to finish - which is waiting for the main thread to return from OnTerminate, which is a deadlock.
How would a thread inform me that it is done if not with OnTerminate?
OnTerminate is fine for informing that a thread has done its job, although you can use other means like using synchronization objects or queuing a procedure or posting a message etc.. Also worth noting that it's possible to wait on a thread handle, which is what TThread.WaitFor does.
In my test program I tried to determine application termination time depending on various exit strategies. All test results are dependent on my testing environment.
Termination time is measured starting from when the OnClose handler of a VCL form is called and ending with just before ExitProcess is called by the RTL. Also, this method does not account for how long ExitProcess takes, which I presume would be different when there are dangling threads. But I didn't try to measure it anyway.
Worker threads query the existence of a directory on a non-existing host. This is the most I could come up on waiting time. Every query is on a new non-existing host, otherwise DirectoryExists returns immediately.
A timer starts and collects worker threads. Depending on the time the IO query takes (which is around 550ms) the timer interval effects the total count of threads at any given time. I tested on around 10 threads with a timer interval of 250ms.
Various debug outputs allow to follow the flow in the event log of the IDE.
My first test was to leave the worker threads behind - just quit the application. The time I measured was 30-65ms. Again, this could have caused ExitProcess itself to take longer.
Next, I tested terminating the threads with TerminateThread. This took 140-160ms. I believe this is actually closer to what the previous test would come up if the time ExitProcess takes could be accounted for. But I have no proof on that.
Next, I tested cancelling the IO request on running threads and then leaving them behind.This considerably decreased the amount of leaked memory, in fact completely eliminated in most of the runs. Although the cancellation request is asynchronous, nearly all of the threads return immediately and find the time to finish. Anyway, this took 160-190ms.
I should note here that the code in DirectoryExists is defective, at least in XE2. The first thing the function does is to call GetFileAttributes. An INVALID_FILE_ATTRIBUTES return denotes the function failed. This is how the RTL handles the fail:
function DirectoryExists(const Directory: string; FollowLink: Boolean = True): Boolean;
...
...
Result := False;
Code := GetFileAttributes(PChar(Directory));
if Code <> INVALID_FILE_ATTRIBUTES then
begin
...
end
else
begin
LastError := GetLastError;
Result := (LastError <> ERROR_FILE_NOT_FOUND) and
(LastError <> ERROR_PATH_NOT_FOUND) and
(LastError <> ERROR_INVALID_NAME) and
(LastError <> ERROR_BAD_NETPATH);
end;
end;
This code assumes that unless GetLastError returns one of the above error codes the directory exists. This reasoning is flawed. Indeed, when you cancel the IO request, GetLastError returns ERROR_OPERATION_ABORTED (995) as documented but DirectoryExists returns true whether the directory exists or not.
Waiting for the threads to finish without cancelling IO takes 330-530ms. This completely eliminates memory leaks.
Cancelling IO requests and then waiting for the threads to finish takes 170-200ms. Of course no memory leaks here either. Considering there are no significant timing difference in any of the options, this would be the one I choose.
Testing code I used is below:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes,
Vcl.Controls, Vcl.Forms, Vcl.ExtCtrls,
generics.collections;
type
TForm1 = class(TForm)
Timer1: TTimer;
procedure Timer1Timer(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure FormDestroy(Sender: TObject);
private
FThreads: TList<TThread>;
end;
var
Form1: TForm1;
implementation
uses
diagnostics;
{$R *.dfm}
type
TIOThread = class(TThread)
private
FTarget: string;
protected
constructor Create(Directory: string);
procedure Execute; override;
public
destructor Destroy; override;
end;
constructor TIOThread.Create(Directory: string);
begin
FTarget := Directory;
inherited Create;
end;
destructor TIOThread.Destroy;
begin
inherited;
OutputDebugString(PChar(Format('Thread %d destroyed', [ThreadID])));
end;
procedure TIOThread.Execute;
var
Watch: TStopwatch;
begin
OutputDebugString(PChar(Format('Thread Id: %d executing', [ThreadID])));
Watch := TStopwatch.StartNew;
ReturnValue := Ord(DirectoryExists(FTarget));
Watch.Stop;
OutputDebugString(PChar(Format('Thread Id: %d elapsed time: %dms, return: %d',
[ThreadID, Watch.Elapsed.Milliseconds, ReturnValue])));
end;
//-----------------------
procedure TForm1.FormCreate(Sender: TObject);
begin
FThreads := TList<TThread>.Create;
Timer1.Interval := 250;
Timer1.Enabled := True;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
FThreads.Free;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
ShareName: array [0..12] of Char;
i: Integer;
H: THandle;
begin
for i := FThreads.Count - 1 downto 0 do
if FThreads[i].Finished then begin
FThreads[i].Free;
FThreads.Delete(i);
end;
for i := Low(ShareName) to High(ShareName) do
ShareName[i] := Chr(65 + Random(26));
FThreads.Add(TIOThread.Create(Format('\\%s\share', [string(ShareName)])));
OutputDebugString(PChar(Format('Possible thread count: %d', [FThreads.Count])));
end;
var
ExitWatch: TStopwatch;
// not declared in XE2
function CancelSynchronousIo(hThread: THandle): Bool; stdcall; external kernel32;
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
i: Integer;
Handles: TArray<THandle>;
IOPending: Bool;
Ret: DWORD;
begin
ExitWatch := TStopwatch.StartNew;
// Exit;
Timer1.Enabled := False;
{
for i := 0 to FThreads.Count - 1 do
TerminateThread(FThreads[i].Handle, 0);
Exit;
//}
if FThreads.Count > 0 then begin
SetLength(Handles, FThreads.Count);
for i := 0 to FThreads.Count - 1 do
Handles[i] := FThreads[i].Handle;
//{
OutputDebugString(PChar(Format('Cancelling at most %d threads', [Length(Handles)])));
for i := 0 to Length(Handles) - 1 do
if GetThreadIOPendingFlag(Handles[i], IOPending) and IOPending then
CancelSynchronousIo(Handles[i]);
//}
//{
Assert(FThreads.Count <= MAXIMUM_WAIT_OBJECTS);
OutputDebugString(PChar(Format('Will wait on %d threads', [FThreads.Count])));
Ret := WaitForMultipleObjects(Length(Handles), #Handles[0], True, INFINITE);
case Ret of
WAIT_OBJECT_0: OutputDebugString('wait success');
WAIT_FAILED: OutputDebugString(PChar(SysErrorMessage(GetLastError)));
end;
//}
for i := 0 to FThreads.Count - 1 do
FThreads[i].Free;
end;
end;
procedure Exiting;
begin
ExitWatch.Stop;
OutputDebugString(PChar(
Format('Total exit time:%d', [ExitWatch.Elapsed.Milliseconds])));
end;
initialization
ReportMemoryLeaksOnShutdown := True;
ExitProcessProc := Exiting;
end.
I am trying to handle a few thousand users through Indy TCP servers but i always saw very high memory consumption even with a few hundred users... i just wrote a bot to test out the performance of the server handling the data. I connected 300 bots to the test server and started sending packets through. The memory usage climbed to a few hundred MB in matter of minutes...
After going through codes i noticed that the main issue was with using sender queue for each thread so each thread can transmit its messages in its Execute function. If 300 users are sending packets to each other and writing data to each thread's queue then it cause the memory to overload... Here is what i am doing and can anyone suggest any better way to handle this?
When a client is sending message to another client this function is called and is supplied with the context of that thread/client/conneciton
Procedure TMainFrm.SendRoomBuffer(Packet: Pointer; Size: Integer; Context: TIdContext);
Var
LocalBuffer: Pointer;
Connected: Boolean;
Begin
If Size < 1 then
Exit;
Try
If Context <> Nil Then
Connected := TRoomContext(Context).Connection.Connected
Else
Connected := False;
Except
Connected := False;
End;
If Connected = True Then Begin
GetMem(LocalBuffer,Size);
CopyMemory(LocalBuffer,Packet,Size);
TRoomContext(Context).Queue.Add(LocalBuffer);
End;
End;
Iterates through all the users present in the room and send them the packet
Lst := Room.UsersList.LockList;
Try
For I := 0 To Lst.Count -1 Do Begin
Try
Username := TRoomUserInfo(Lst.Items[I]).UserName.Value;
If IncludingMe = False Then Begin
If LowerCase(Username) <> LowerCase(MyNick) Then
SendRoomBuffer(Packet, PacketSize, TRoomUserInfo(Lst.Items[I]).Context)
End Else
SendRoomBuffer(Packet, PacketSize, TRoomUserInfo(Lst.Items[I]).Context);
Finally
Username := '';
End;
End;
Finally
Room.UsersList.UnlockList;
Lst := Nil;
End;
This is where the actual sending is done, in the Execute of IdTCPServer
If Not TRoomContext(AContext).Queue.IsEmpty Then Begin
tmpQueue := TRoomContext(AContext).Queue.LockList;
Try
While tmpQueue.Count > 0 Do Begin
outBuffer := tmpQueue.items[0];
Try
outLen := PCommunicatorPacket(outBuffer).BufferSize;
SetLength(outBuf,outLen);
Try
CopyMemory(#outBuf[0],outBuffer,outLen);
Try
If Connected Then
AContext.Connection.IOHandler.Write(outBuf)
Finally
tmpQueue.Delete(0);
End;
Finally
SetLength(outBuf,0);
outBuf := Nil;
End;
Finally
If outBuffer <> Nil Then Begin
FreeMem(outBuffer);
outBuffer := Nil;
End;
End;
End;
Finally
TRoomContext(AContext).Queue.UnlockList;
tmpQueue := Nil;
End;
End;
Complete OnExecute Function
Procedure TMainFrm.RoomSckExecute(AContext: TIdContext);
Var Buf: TIdBytes;
Len: Integer;
outBuffer: PIdBytes;
tmpQueue: TList;
Begin
AContext.Connection.IOHandler.CheckForDataOnSource(10);
if not AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
Len := AContext.Connection.IOHandler.InputBuffer.Size;
AContext.Connection.IOHandler.ReadBytes(Buf, Len, False);
TRoomContext(AContext).ProcessPacket(#Buf[0], Len, AContext);
SetLength(Buf, 0);
Buf := nil;
end;
tmpQueue := TRoomContext(AContext).Queue.LockList;
try
while tmpQueue.Count > 0 do begin
outBuffer := PIdBytes(tmpQueue.Items[0]);
try
tmpQueue.Delete(0);
AContext.Connection.IOHandler.Write(outBuffer^);
finally
Dispose(outBuffer);
end;
end;
finally
TRoomContext(AContext).Queue.UnlockList;
end;
End;
If a single client is sending to 300 clients present in the room then 300 copies of the packets are made and are freed only when the actual sending is done...
If i do the writing directly to each connection and not by using queues then the memory consumption is not as rogue as this method but server hangs after a few minutes
Sorry if i forgot to mention any more details.
P.S: I am using Delphi 7
EDIT: I just check, if i dont actually write to socket, and go with the whole process as is, then the issue doesn't happen... so it means the time it takes to write to the socket, there are over a few hundred more packets read...
EDIT 2 I copied your code for the OnExecute, if i don't prove a length to ReadBytes then it takes some time about 3-5 seconds to process each command, so i am providing it with the length to read... And i used madexcept it doesnt show any leaks, i am gonna try with FastMM too now... but if there was actually a leak and something was causing it then why would commenting out the actual Write command in OnExecute suppress the memory usage?
EDIT 3 To explain my question further, i am actually reading the bytes from the stream and then process them myself later to make distinct packets from them, here is the code of what happens further after the data is read from the socket.
...
FPacketBuffer: Pointer; // global memory upto 65kb for each client to store the incoming data
PacketBufferPtr: Integer; // the offset upto where the data is read from the global memory
...
procedure TRoomContext.ProcessPacket(Buffer: Pointer; BufSize: Integer; Context: TIdContext);
begin
AddToPacketBuffer(Buffer,BufSize);
CheckAndProcessPacket(Context);
end;
procedure TRoomContext.AddToPacketBuffer(Buffer: Pointer; Size: Integer);
var
DestPtr: Pointer;
begin
if PacketBufferPtr + Size<65536 then
begin
DestPtr := Pointer(Cardinal(FPacketBuffer)+Cardinal(PacketBufferPtr));
Move(Buffer^,DestPtr^,Size);
PacketBufferPtr := PacketBufferPtr + Size;
end
else
begin
end;
end;
procedure TRoomContext.CheckAndProcessPacket(Context: TIdContext);
var DestPtr: Pointer;
NewPacketBufferLen: Integer;
SharedBuff: Pointer;
begin
if PCommunicatorPacket(FPacketBuffer).Signature = PACKET_SIGNATURE then
begin
while PCommunicatorPacket(FPacketBuffer).BufferSize <= PacketBufferPtr do
begin
GetMem(SharedBuff,PCommunicatorPacket(FPacketBuffer).BufferSize);
Try
CopyMemory(SharedBuff,FPacketBuffer,PCommunicatorPacket(FPacketBuffer).BufferSize);
MainFrm.ExecuteRoomPacket(SharedBuff, Context);
Finally
If SharedBuff <> Nil Then FreeMem(SharedBuff);
End;
NewPacketBufferLen := PacketBufferPtr - PCommunicatorPacket(FPacketBuffer).BufferSize;
DestPtr := Pointer(Cardinal(FPacketBuffer)+PCommunicatorPacket(FPacketBuffer).BufferSize);
Move(DestPtr^, FPacketBuffer^, NewPacketBufferLen);
PacketBufferPtr := NewPacketBufferLen;
end;
end
else
begin
DropInvalidPacket;
Inc(InvalidPackets);
If InvalidPackets > 50 Then
Context.Connection.Disconnect;
Exit;
end;
end;
Apologies for thinking it was because of the writing, the writing actually just slowed deletion from queue which made me think so, if i even put a sleep of 10 milliseconds, the memory consumption go rogue. About the leaks... one other reason i think this is not a leak is because if i stop the bots from messaging further, then the used memory gets back to where it was, but if i leave it running for a few minutes then it goes to a point where the application hangs or i receive an out of memory message. I think the issue is with making copies, i tried using a global queue for the room to handle messages and so multiple copies aren't made of the data, but that cause the application to hang after sometime maybe too much thread contention or i am not playing it safe.
TCP does not support broadcasting, and directly writing to TIdTCPServer connections from outside the server's events is generally not thread-safe (although it can be done if you are careful). In your situation, using queues is a good idea.
However, don't call Connected() in SendRoomBuffer(). It performs a read operation, which can interfere with any reading the OnExecute event handler performs, and can corrupt the InputBuffer's content by reading socket data out of order. If Context is not nil then queue the data regardless of the socket state, and catch any errors.
Also, in the OnDisconnect event, make sure you are freeing any queued packets that were not sent, otherwise you will leak them.
Lastly, your OnExecute code is making another copy of the queued data and then sending that copy (I am assuming that outBuf is a TIdBytes). Try to avoid that. I would suggest you change your queue to store TMemoryStream or TIdBytes objects instead of raw memory blocks, then you can pass the queued items directly to IOHandler.Write() without having to make copies of them first.
Try something like this:
type
PIdBytes = ^TIdBytes;
procedure TMainFrm.SendRoomBuffer(Packet: Pointer; Size: Integer; Context: TIdContext);
var
LocalBuffer: PIdBytes;
begin
if (Packet = nil) or (Size < 1) or (Content = nil) then
Exit;
New(LocalBuffer);
try
LocalBuffer^ := RawToBytes(Packet^, Size);
TRoomContext(Context).Queue.Add(LocalBuffer);
except
Dispose(LocalBuffer);
end;
end;
Lst := Room.UsersList.LockList;
try
for i := 0 To Lst.Count -1 do begin
Username := TRoomUserInfo(Lst.Items[i]).UserName.Value;
if (not IncludingMe) and TextIsSame(Username, MyNick) then begin
Continue;
end;
SendRoomBuffer(Packet, PacketSize, TRoomUserInfo(Lst.Items[i]).Context);
end;
finally
Room.UsersList.UnlockList;
end;
procedure TMainFrm.RoomSckDisconnect(AContext: TIdContext);
var
tmpQueue: TList;
i: Integer;
begin
...
tmpQueue := TRoomContext(AContext).Queue.LockList;
try
for i := 0 to tmpQueue.Count-1 do begin
Dispose(PIdBytes(tmpQueue.Items[i]));
end;
tmpQueue.Clear;
finally
TRoomContext(AContext).Queue.UnlockList;
end;
...
end;
procedure TMainFrm.RoomSckExecute(AContext: TIdContext);
var
Buf: TIdBytes;
outBuffer: PIdBytes;
tmpQueue: TList;
begin
AContext.Connection.IOHandler.CheckForDataOnSource(10);
if not AContext.Connection.IOHandler.InputBufferIsEmpty then
begin
AContext.Connection.IOHandler.ReadBytes(Buf, -1, False);
TRoomContext(AContext).ProcessPacket(#Buf[0], Len, AContext);
SetLength(Buf, 0);
Buf := nil;
end;
tmpQueue := TRoomContext(AContext).Queue.LockList;
try
while tmpQueue.Count > 0 do begin
outBuffer := PIdBytes(tmpQueue.Items[0]);
try
tmpQueue.Delete(0);
AContext.Connection.IOHandler.Write(outBuffer^);
finally
Dispose(outBuffer);
end;
end;
finally
TRoomContext(AContext).Queue.UnlockList;
end;
end;
Long story short, I'm very far from a skilled programmer, in fact, my most complicated programs so far were either plain ASCII string manipulating, simple maths and array searching/sorting either in Free Pascal or later, Delphi 7 and Java. This was some years ago, when I learnt programming in high school faculty (that was plain Pascal). Later I went on to become a programmer (meeting with D7 and Java, and some C++), but I had quit my programming studies due to personal reasons, and since then, I didn't wrote a single line of code.
Erm, sorry for the long introduction, so... Recently I decided to revive programming as my hobby, mainly because I didn't found a suitable program for some tasks I would like to accomplish since long. In spite of my faint understanding of such fairly basic things as explicit parameters, pointers, objects, classes, constructors and threads, with the help of a programming book, Delphi help files and the internet, I managed to write a simple program in Delphi 7 that can load and display certain image file formats in a given directory using external libraries, make it possible to arbitrarily switch among them (using the GUI), and log some information (mainly for debug purposes) in text files.
However, I encountered a problem at the current version of the code when I tried to make the image loading and displaying function threaded. First, for better understanding, I'll explain the logic of my program.
First of all, in the main form's FormCreate event, the program looks for supported image files in the current directory (where the exe is). If there is no image files, the current directory is set at the one at the upper level (with the standard Windows file system symbol "..") and is checked for images. Supported image files are determined by file extensions. Anyway, this explorer function stores the found images' filename and a file type identifier (which is a byte) in a dynamic array. Then, using this array as a reference, the first supported image file is loaded by the correct library and displayed in the form. The GUI has buttons and a combobox to change between images, with each control's OnClick or OnSelect (combobox) event setting variables about the supposedly current image file and calling the image loader and displayer function which uses the reference array.
The problem is that some images are so huge that the loading takes noticeable time, so the GUI can't respond until the image is fully loaded and displayed. I tried to make this program threaded by initializing each image loader function as a thread. While the GUI is more responsive now, there are certainly two new bugs with the program.
The first is that the program sometimes randomly crashes when changing images, with appearing messages either referring to "JPEG Error #58" (supposedly meaning "invalid file structure" in Delphi's in-built jpeg library), "EAccessViolation" exception, "EOSError" exception (including "System Error, Code 5"), "unknown software exception", "Runtime error 216", and error messages about memory locations and failed read operations. Before using threads, none of these error messages appeared, but I certainly want to (and must) use threads in the program.
The other, minor bug is that when the interface buttons are clicked in a fast succession, it seems like all loading and displaying takes place, although in a laggy-then-quickly manner. I don't really have an idea on how to "kill" a thread and initiate it "again" to load the now-current file instead of the obsolete one it tried to load a few hundred milliseconds ago.
I start a thread in the following manner:
LoaderThread := CreateThread(nil, 0, Addr(LoadPicture), nil, 0, LoaderThreadID);
CloseHandle(LoaderThread);
I use this two times in the main form's FormCreate event (although only one of them executes at any start), and in the GUI controls' OnClick or OnSelect event to faciliate the desired function of the control (for example skip to the last image).
Any suggestions? Thank you in advance! :)
UPDATE:
Here is some (well, almost all) of my source code:
procedure TMainForm.FormCreate(Sender: TObject);
begin
MainForm.DoubleBuffered := true;
MainJPEG := TJPEGImage.Create;
MainJPEG.ProgressiveDisplay := true;
MainJPEG.Smoothing := true;
MainJPEG.Performance := jpBestQuality;
MainPNG := TPNGObject.Create;
MainGIF := TGIFImage.Create;
AssignFile(Log, '_NyanLog.txt');
CurrentDir := GetCurrentDir;
ExploreCurrentDir;
if CurrentDirHasImages = false then
begin
SetCurrentDir('..');
CurrentDir := GetCurrentDir;
ExploreCurrentDir;
end;
if CurrentDirHasImages = true then
begin
CurrentFilename := ImagesOfCurrentDir[CurrentPos].Filename;
CurrentFiletype := ImagesOfCurrentDir[CurrentPos].Filetype;
LoaderThread := BeginThread(nil, 0, Addr(LoadImage), nil, 0, LoaderThreadID);
CloseHandle(LoaderThread);
if Length(ImagesOfCurrentDir) > 1 then
begin
MainForm.NextButton.Enabled := true;
MainForm.EndButton.Enabled := true;
MainForm.SlideshowButton.Enabled := true;
MainForm.SlideshowIntervalUpDown.Enabled := true;
end;
UpdateTitleBar;
end
else UpdateTitleBar;
end;
procedure ExploreCurrentDir;
var
Over: boolean;
begin
CurrentPos := 0;
Over := false;
ReWrite(Log);
Write(Log, 'blablabla');
if FindFirst(CurrentDir+'\*.*', faAnyFile-faDirectory, Find) = 0 then
begin
CurrentFilename := Find.Name;
DetermineFiletype;
if CurrentFiletype <> UNSUPPORTED then
begin
SetLength(ImagesOfCurrentDir, CurrentPos+1);
ImagesOfCurrentDir[CurrentPos].Filename := CurrentFilename;
ImagesOfCurrentDir[CurrentPos].Filetype := CurrentFiletype;
MainForm.ImagelistComboBox.AddItem(CurrentFilename, nil);
Write(Log, 'blablabla');
CurrentPos := Succ(CurrentPos);
end;
while Over = false do
begin
if FindNext(Find) = 0 then
begin
CurrentFilename := Find.Name;
DetermineFiletype;
if CurrentFiletype <> UNSUPPORTED then
begin
SetLength(ImagesOfCurrentDir, CurrentPos+1);
ImagesOfCurrentDir[CurrentPos].Filename := CurrentFilename;
ImagesOfCurrentDir[CurrentPos].Filetype := CurrentFiletype;
MainForm.ImagelistComboBox.AddItem(CurrentFilename, nil);
Write(Log, 'blablabla');
CurrentPos := Succ(CurrentPos);
end;
end
else
begin
FindClose(Find);
Over := true;
end;
end;
CurrentDirImageCount := Length(ImagesOfCurrentDir);
CurrentDirHasImages := true;
Write(Log, 'blablabla');
end;
if CurrentDirHasImages = false then Write(Log, 'blablabla');
CloseFile(Log);
CurrentPos := 0;
end;
procedure LoadImage; //procedure #1 which should be used in a thread
begin
if CurrentFiletype = BMP then
begin
MainForm.MainImage.Picture := nil;
MainForm.MainImage.Picture.LoadFromFile(CurrentFilename)
end
else
if CurrentFiletype = JPEG then
begin
MainForm.MainImage.Picture := nil;
MainJPEG.LoadFromFile(CurrentFilename);
MainForm.MainImage.Picture.Assign(MainJPEG);
end
else
if CurrentFiletype = PNG then
begin
MainForm.MainImage.Picture := nil;
MainPNG.LoadFromFile(CurrentFilename);
MainForm.MainImage.Picture.Assign(MainPNG);
end
else
if CurrentFiletype = GIF then
begin
MainForm.MainImage.Picture := nil;
MainGIF.LoadFromFile(CurrentFilename);
MainForm.MainImage.Picture.Assign(MainGIF);
end;
end;
procedure NextImage; //the "NextButton" button from the GUI calls this
begin
if CurrentPos < Length(ImagesOfCurrentDir)-1 then
begin
CurrentPos := Succ(CurrentPos);
CurrentFilename := ImagesOfCurrentDir[CurrentPos].Filename;
CurrentFiletype := ImagesOfCurrentDir[CurrentPos].Filetype;
UpdateTitleBar;
LoaderThread := BeginThread(nil, 0, Addr(LoadImage), nil, 0, LoaderThreadID);
CloseHandle(LoaderThread);
while MainImageIsEmpty = true do
begin
if CurrentPos < Length(ImagesOfCurrentDir)-1 then
begin
CurrentPos := Succ(CurrentPos);
CurrentFilename := ImagesOfCurrentDir[CurrentPos].Filename;
CurrentFiletype := ImagesOfCurrentDir[CurrentPos].Filetype;
UpdateTitleBar;
LoaderThread := BeginThread(nil, 0, Addr(LoadImage), nil, 0, LoaderThreadID);
CloseHandle(LoaderThread);
end;
if CurrentPos = CurrentDirImageCount-1 then Break;
end;
end;
if CurrentPos = CurrentDirImageCount-1 then
begin
MainForm.NextButton.Enabled := false;
MainForm.EndButton.Enabled := false;
MainForm.SlideshowButton.Enabled := false;
MainForm.SlideshowIntervalUpDown.Enabled := false;
end;
MainForm.PrevButton.Enabled := true;
MainForm.StartButton.Enabled := true;
end;
procedure PrevImage; //called by "PrevButton"
begin
//some code, calls LoadImage
//almost the same logic as above for a backward step among the images
end;
procedure FirstImage; //called by "StartButton"
begin
//some code, calls LoadImage
end;
procedure LastImage; //called by "EndButton"
begin
//some code, calls LoadImage
end;
procedure Slideshow; //procedure #2 which should be used in a thread
begin
while SlideshowOn = true do
begin
SlideshowInterval := MainForm.SlideshowIntervalUpDown.Position*1000;
Sleep(SlideshowInterval);
NextImage; //NextImage calls LoadImage which should be a thread
if CurrentPos = CurrentDirImageCount-1 then SlideshowOn := false;
end;
end;
function MainImageIsEmpty;
begin
if MainForm.MainImage.Picture = nil then MainImageIsEmpty := true
else MainImageIsEmpty := false;
end;
procedure TMainForm.NextButtonClick(Sender: TObject);
begin
NextImage;
end;
procedure TMainForm.PrevButtonClick(Sender: TObject);
begin
PrevImage;
end;
procedure TMainForm.StartButtonClick(Sender: TObject);
begin
FirstImage;
end;
procedure TMainForm.EndButtonClick(Sender: TObject);
begin
LastImage;
end;
procedure TMainForm.SlideshowButtonClick(Sender: TObject);
begin;
if SlideshowOn = false then
begin
SlideshowOn := true;
SlideshowThread := BeginThread(nil, 0, Addr(Slideshow), nil, 0, SlideshowThreadID);
SlideshowButton.Caption := '||';
SlideshowButton.Hint := 'DIAVETÍTÉS LEÁLLÍTÁSA';
end
else
begin
SlideshowOn := false;
CloseHandle(SlideshowThread);
SlideshowButton.Caption := '|>';
SlideshowButton.Hint := 'DIAVETÍTÉS INDÍTÁSA';
end;
end;
There's a lot of text here, and not much code. Your question would probably be better with more code and less text.
Anyway, I can offer some hints.
Firstly, calling CreateThread directly is a rather laborious way to do threading in Delphi. It's easier to use TThread which wraps up some of the low-level Windows API issues in a manner more native to typical Delphi code style. Of course, you could go further and use a threading library like OmniThreadLibrary, but for now it may be better just to stick to TThread and work out how to do it that way.
Now, that won't be your problem here. Almost certainly your problem will be cause by one of two common issues with threading:
All VCL and GUI code should run in the main thread. Windows controls have affinity with the thread that creates them. Many parts of the VCL are not thread-safe. These issues strongly push you to putting all VCL/GUI code in the main thread.
It's quite possible that you have a race condition due to lack of synchronisation.
The most common way to deal with issue 1 is to call TThread.Synchronize or TThread.Queue from the worker threads in order to force all the VCL/GUI code to run on the main thread. Of course you need to be sure that none of the time-consuming code in your worker thread uses VCL/GUI objects since that is doomed to failure.
Issue 2 can be dealt with by synchronisation objects like critical sections or lock-free methods using the InterlockedXXX family of functions.
Exactly what your problem is I can't say. If you want more detailed help then please post more code, most probably cut down from what you are currently running.
You create a thread and kill it right away without waiting for it to finish loading
LoadImage is not VCL thread safe
Here simple seudo thread in VCL way. Codes is in simple form and you can study further and make enhancement
TYourThread.Create(image file name);
type
TYourThread = class(TThread)
protected
FBitmap: TBitmap;
FImageFileName: string;
procedure BitmapToVCL;
begin
MainForm.MainImage.Picture := FBitmap;
end;
procedure Execute; override;
begin
FBitmap := TBitmap.Create;
try
FBitmap.LoadFromFile(FImageFileName);
Synchronize(BitmapToVCL);
finally
FreeAndNil(FBitmap);
end;
end;
public
constructor Create(const AImageFileName: string);
begin
FImageFileName := AImageFileName;
inherited Create(False);
FreeOnTerminate := True;
end;
end;
Gook luck
Cheer