Why the task cancellation happens on the caller thread? Why the task cancellation happens on the caller thread? multithreading multithreading

Why the task cancellation happens on the caller thread?


Which thread a task runs on is an implementation detail. One you could only ever nail down if you use a task scheduler that knows how to run code on a specific thread. Like TaskScheduler.FromCurrentSynchronizationContext(). Which will never work in a console mode app since it doesn't have one.

So it is up to the Task class implementation to figure out what thread to use. And it will look for an opportunity to not require a thread context switch, those are expensive. If it has a choice between starting a threadpool thread to execute code and waiting for it to complete vs executing the code directly then it always will pick the last choice, it is superior.

It found one in your code, you called the Abort() method on your main thread. Which, through lots of layers in the Task class plumbing (look at the Call Stack window), figured out how to call the finally block on the same thread. This is a Good Thing of course. And one you should expect, your thread doesn't have anything else to do so it might as well be used to execute task code.

Compare to using CancelAfter(), now your thread is not suitable to execute the finally block and you'll see the finally block execute on a TP thread.


It seems that once you call Cancel() on the first child thread, the await continuation cannot longer resume back on this thread, and instead executes on the caller/parent thread. If you add a catch right after the call to spawn the second child thread, you can see the code executed by the parent thread after a TaskCancelationException,

try{    bool result = await c.MyTask(cts.Token);    return result;}catch (Exception exception){    Console.WriteLine("catch invoker exception=" + exception.GetType());    Console.WriteLine("catch invoker=" + Thread.CurrentThread.ManagedThreadId);    return true;}

Which produces,

program=10begin worker=11begin invoker=11begin task=11canceling=10catch invoker exception=TaskCanceledExceptioncatch invoker=10      <-- parent thread resuming on child cancellationremoving=10

The reason why it executes on the parent thread might be an implementation detail due to performance reasons of spawning a new thread to resume the execution (what Hans Passant explained); similarly, if the child thread is never canceled (comment out c.Abort();), the await execution will resume in both cases on the child thread, rather than the parent,

program=10begin worker=11   <-- first child threadbegin invoker=11begin task=11Press any key...end task=12       <-- second child thread resuming on 'await Task.Delay'removing=12       <-- second child thread resuming on 'await c.MyTask(cts.Token)'end invoker=12end worker=True    end worker=11     <-- back to the first child thread

Where thread 11, which already returned back to its caller method (back at Worker), might prove more expensive to switch thread context to resume at MyTask, whereas thread 12 (the supposed second child), has just become available for the continuation, but only up to the Invoker methods' end, where thread 11 is at the exact place it was originally suspended.