0

Consider the code below

static void Main(string[] args)
{
  var ts = new CancellationTokenSource();
  CancellationToken ct = ts.Token;

  Task<string> task = Task.Run(() =>
    {
      ct.ThrowIfCancellationRequested();
      var task2 = ActualAsyncTask();
      while (!task2.IsCompleted)
      {
        var t = DateTime.Now;
        while (DateTime.Now - t < TimeSpan.FromSeconds(1))
        {
        }
        ct.ThrowIfCancellationRequested();
      }
      return task2;
    }, ct);

  Console.ReadLine();
  ts.Cancel();
  Console.ReadLine();
}

static async Task<string> ActualAsyncTask()
{
  await Task.Delay(1000);
  for(int i = 0; i < 100; ++i)
  {
    var t = DateTime.Now;
    while (DateTime.Now - t < TimeSpan.FromSeconds(1))
    {
    }
    Console.WriteLine("tick");
  }
  return "success";
}

It spawns a task that busy-waits for a cancellation request while an asynchronous method also busy-waits and prints some text into console.

When you let it run for a few seconds and then press enter, the task will throw the cancellation exception. While it's an interesting trick, I don't understand how the async method is able to yield control and make it possible.

Both the anonymous lambda within the task and the asynchronous method report to run on the same worker thread, which means they should be synchronous. In my understanding this setup should not throw the cancellation exception past the first 1 second await, because past that point there are no more awaits to allow the while loop of the anonymous lambda to gain control of the thread flow, yet somehow they seemingly both run in parallel using one thread.

If I remove the await command entirely, the execution becomes as expected - sending a cancellation request no longer interrupts the task. What am I missing?

  • 2
    `Task.Run(` isnt awaited, also this code is a mess – TheGeneral Sep 21 '21 at 10:13
  • 1
    "Both the anonymous lambda within the task and the asynchronous method report to run on the same worker thread" - I'd be interested to know how you reached that conclusion. The most obvious would be to also write managed thread IDs to the console but this code doesn't do that. (And note, if you do that, make sure to do it after the `await`, not only before it) – Damien_The_Unbeliever Sep 21 '21 at 10:14
  • Is this a learning experiment, or it's code that you actually intend to deploy in production? – Theodor Zoulias Sep 21 '21 at 10:25
  • @Damien_The_Unbeliever I've reached that conclusion by opening a debugger thread view in visual studio and viewing which thread the code is executed in for both the async method and the anonymous lambda. Do you have reasons to believe it is misleading me? – CodeMolester66 Sep 21 '21 at 10:31
  • @TheodorZoulias neither. It is a stripped down minimal repro of code that already exists in production – CodeMolester66 Sep 21 '21 at 10:33
  • Yes, because you don't get two different pieces of code running at the same time on a single thread. So going back to my previous comment, did you do that check *after* `ActualAsyncTask` had gone past the `await` or only at the start of the method? – Damien_The_Unbeliever Sep 21 '21 at 10:34
  • @Damien_The_Unbeliever after await. The breakpoints were placed on the lines `while (DateTime...` in both methods – CodeMolester66 Sep 21 '21 at 10:37
  • I think it's fair to say that your code-base could be improved, by learning how to [asynchronously wait for `Task` to complete with timeout](https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout). – Theodor Zoulias Sep 21 '21 at 10:48
  • 1
    No repro for me. On the first breakpoint, the anonymous method was running on managed ID 4. On the second breakpoint, `ActualAsyncTask` was running on managed ID 5, and managed ID 4 was deep in `TimeZoneInfo` with a call stack still just containing the anonymous method. They're not running on the same thread so any conclusion you draw based on that is bound to be incorrect. – Damien_The_Unbeliever Sep 21 '21 at 10:59
  • @Damien_The_Unbeliever thank you for pointing this out. Upon rechecking this, they really do run on different threads, so it makes sense. I can finally understand why this causes the issues it causes. – CodeMolester66 Sep 22 '21 at 07:31

0 Answers0