I wanted a clean way to spin up a looping task in C# and do things while the task is running.

Conceptually, I want

1. Start the “service”
2. Run procedure
3. 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 Thread.Sleep and 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.

Turns out 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:

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 IDisposable).

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 Task’s Wait():

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: