Disposable Background Tasks in C#
I wanted a clean way to spin up a looping task in C# and do things while the task is running.
Conceptually, I want
- Start the “service”
- Run procedure
- Stop the “service”
This would be simple if the “service” had a start / stop button. However, the “service” runs like dead man’s switch: unless it’s pinged every now and then, it dies. When it’s first pinged, it starts up. Specifically, what I had was:
- A “service” that dies if I don’t ping it every 10 seconds
- A procedure that need to be run against the “service” while it’s alive
I needed a clean way to write my routine without resorting to
for loop hell. The obvious answer’s threading – put the timer and ping on a background thread, run the procedure, then kill the thread. This is probably simple using the Task Parallel Library in C#. However, I did not want to do so imperatively and start / stop the thread explicitly; doing so results in uglier / less readable code.
IDisposable gives me a clean way to do this:
This produces the output:
Essentially, I wrapped the action I needed to do (in this case, a simple print to console
() => Console.WriteLine("DisposableThread running") in a task:
async () =>
If you extract this lambda into its own method, it would return a
Task encapsulating encapsulating the multiple possible
awaits. The lambda holds on to the cancellation token
token that allows us to act on cancellation requests (which can then be issued by the
Dispose() method of
Additionally, it’s important that we use
Task.Delay(delayMilliseconds, token) instead of
Thread.Sleep(delayMilliseconds). This allows us to react to cancellations during the delay instead of after the delay.
Finally, the dispose method needs to call
public void Dispose()
When we call
_cancellationTokenSource.Cancel(), we are cancelling the task(s) that hold on to the token. Each of the tasks then throws a
TaskCanceledException. This exception is not thrown in the main thread until it’s either handled explicitly or the garbage collector collects and finalizes the object. That means if the
TaskCanceledException isn’t handled explicitly and the main thread goes on long enough, we may encounter an exception out of nowhere. Hence, we force the exception (which, like all
Task exceptions, gets rolled into an
AggregateException) by calling
_task.Wait() and catch the exception explicitly.
This allows me to write better looking code like this:
without worrying about starting and stopping the background pinging thread.