Hi,
I've got an app that launches a few threads. Each thread does different things but they all write in the same log file. Some threads check the internet too.
Everything works well in the normal course of the application.
The problem I have is when the user tries to shut down the app. What happens is that those threads that are attempting a web query or are writing in the log file raise an AV complaining that another process is attempting to write the log file. The writing process to log file is thread-safe for sure.
Then, I created a global var to indicate the background running tasks. I use Atomic Inc/Dec to change the value and in FormCloseQuery I am waiting for the value to get to zero before the form is allowed to close.
But the problem with the log file still appears.
What's the strategy to shut down a multi-thread app?
I also contemplated the idea of having another global var to indicate that the app is shutting down and then exit from the threads but this will pollute the code and it does not feel right.
.NET has a nice construct for this, Cancellation Tokens. I created a delphi implementation a while ago
https://github.com/VSoftTechnologies/VSoft.CancellationToken
It's an abstraction around an event, where the calling thread owns the CancellationTokenSource (which has the cancel method) and the threads are passed the CancellationToken - which has the IsCancelled method you can interrogate, and the Handle that can be passed into api calls that take waithandles (like WaitforMultipleObjects).
I have used the cancellation tokens in this library to make http calls cancellable
https://github.com/VSoftTechnologies/VSoft.HttpClient
It's also used in my https://github.com/VSoftTechnologies/VSoft.Awaitable async/await library (a abstraction over OmniThreadLibrary)
All of the above are used in https://github.com/DelphiPackageManager/DPM - any methods that are potentially long running or might need to be cancelled take in an ICancellationToken - so for example in the command line tool, invocking Ctrl+C does this
class procedure TDPMConsoleApplication.CtrlCPressed;
begin
FLogger.Information('Ctrl-C detected.');
FCancellationTokenSource.Cancel;
end;
That's all that's needed (from the outside at least) to cancel the task - and then in the tasks we pass the cancellation token
function TInstallCommand.Execute(const cancellationToken : ICancellationToken) : TExitCode;
begin
// code deleted for brevity.
if not FPackageInstaller.Install(cancellationToken, TInstallOptions.Default) then
result := TExitCode.Error
else
result := TExitCode.OK;
end;
and in long running methods or tight loops
for platform in platforms do
begin
if cancellationToken.IsCancelled then
exit(false);
...
or
objHandles[0] := processInfo.hProcess;
objHandles[1] := cancellationToken.Handle;
{ Wait for Something interesting to happen }
waitRes := WaitForMultipleObjects(2, @objHandles, False, timeoutDuration);