The code discussed here is written in C# and executed with .netcore 3.1
I have the following piece of code, which starts a workload in the background without awaiting for it to complete (fire and forget):
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
try
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
}
finally
{
// here I need to release the resource. Releasing this resource is important and must be done as soon as possible
await resource.DisposeAsync();
}
}, token);
}
There are three important points:
- the background work is started in a fire and forget manner. I'm not interested in awaiting its completion
- the provided cancellation token is important and the background work must listed to incoming cancellation requests
- the provided resource (
IAsyncDisposable) must be released as soon as possible, regardless of the outcome of the background work. In order to release the resource a call toDisposeAsyncis required.
The problem with this code is that the cancellation token is passed to Task.Run invokation. If the token is canceled before the execution of the async delegate starts, the async delegate is never executed and so the finally block is never executed. By doing so the requirement of releasing the IAsyncDisposable resource is not met (basically, DisposeAsync is never called).
The simplest way to solve this issue is not providing the cancellation token when Task.Run is invoked. That way, the async delegate is always executed and so the finally block is executed too. The code inside the async delegate listens to cancellation requests, so the requirement of cancel the execution is met too:
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
try
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
}
finally
{
// here I need to release the resource. Releasing this resource is important and must be done as soon as possible
await resource.DisposeAsync();
}
}, CancellationToken.None);
}
I'm asking myself whether the release of the IAsyncDisposable resource should, instead, be delegated to a continuation task. The code refactored by using this approach is the following:
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
},
token).ContinueWith(async _ =>
{
// release the IAsyncDisposable resource here, afte the completion of the antecedent task and regardless
// of the antecedent task actual state
await resource.DisposeAsync();
});
}
I'm not really familiar with ContinueWith gotchas, so my questions are the following:
- do I have the guarantee that the continuation is always executed, even if the cancellation token is canceled before the execution of the antecedent task starts ?
- is there any issue in providing an async delegate to the invokation of
ContinueWith? Is the execution of the async delegate fully completed as expected ? - What is the best approach ? Passing
CancellationToken.Noneto the invokation ofTask.Run, or relying on the continuation by usingContinueWith?
IMPORTANT NOTE: I know that using Task.Run is not the best approach in a server application (more on that can be found here), so there are probably much better ways of designing my overall architecture. I posted this question to better understanding the actual behavior of ContinueWith, because I'm not really familiar with its usage (in modern .NET code it is largely replaced by the usage of async await).