locked
Await and Canceled Task RRS feed

  • Question

  • I've found some contradiction between "Task-based Asynchronous Pattern" document and my own experiments. Quote: "Any code asynchronously waiting for a canceled task through use of language features will continue execution and receive an OperationCanceledException. " In my code I've implemented TAP method using task with support for cancelation as follows:

    static Task<int> CalculateAsync (CancellationToken ct)
    {
     return Task.Factory.StartNew(() =>
     {
     for (int i = 1; i <= 100; i++)
     {			
      ct.ThrowIfCancellationRequested();	
      Thread.Sleep(20);
     }
     return 123;
     }, ct);
    }
    
    
    

    Somewhere else I call this method using await:

    int result = await CalculateAsync(ct);
    Console.WriteLine("Calculate task finished: {0}", result);
    
    

     But if I call Cancel on CancellationTokenSource (from which CancellationToken ct is) no exception is thrown and the code following await simply doesn't execute. Am I missing something ? Or is there some mistake in document or bug in CTP ?

    Tuesday, November 2, 2010 6:22 PM

Answers

  • Hi Vlada-

    I see.  What's happening is that the OperationCanceledException is getting thrown out of async method, but because the exception is happening at a location where it was called back into as the result of an async operation completing, the exception is escaping into the continuation task used off of the awaited task.  That task captures that exception, and the exception gets eaten.  We'll investigate whether there's something better we could do here, but what do you expect to happen in this case?  Your method is returning void, and there's no current SynchronizationContext, which means there's no means by which to somehow hand that exception back to code you write that cares about the exception.  Primary options would be to eat it or crash.

    Wednesday, November 3, 2010 3:07 PM
    Moderator
  • Thanks for the feedback.

    • Marked as answer by Vlada Bodecek Thursday, November 4, 2010 12:45 AM
    Wednesday, November 3, 2010 11:58 PM
    Moderator

All replies

  • Hi Vlada-

    Where is the "somewhere else" that you're calling CalculateAsync... it's not the Main method of a console application, is it?

    Tuesday, November 2, 2010 7:56 PM
    Moderator
  • Hi Stephen,

    I have a static async method called TestCalculate which calls "int result = await CalculateAsync();". And this TestCalculate is called from Main method of console app. Is this important? If so I'm afraid I really missed something :-(

    Vlada

    Tuesday, November 2, 2010 8:24 PM
  • Vlada,

     

    Try the following:

     

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Net;
    using System.IO;
    using System.Threading;
    using System.Diagnostics;
    
    namespace CSharpConsole
    { 
      class Program
      {
        static Task<int> CalculateAsync(CancellationToken token)
        {
          return Task.Factory.StartNew(() =>
            {
              for (int i = 0; i < 100; ++i)
              {
                token.ThrowIfCancellationRequested();
                Thread.Sleep(100);
              }
              return 42;
            });
        }
    
        static async void Test(CancellationTokenSource cts)
        {
          try
          {
            var task = CalculateAsync(cts.Token);
            int result = await task;
    
            Console.WriteLine(result);
          }
          catch (OperationCanceledException e)
          {
            Console.WriteLine("Canceled! {0}", e.Message);
          }
        }
    
        static void Main(string[] args)
        {
          CancellationTokenSource cts = new CancellationTokenSource();
          Test(cts);
          cts.Cancel();
          Console.WriteLine("Press any key to exit...");
          Console.ReadKey();
        }
      }
    }
    

    This adapts your code to show that the cancelation does work.

     

    I suspect the problem is that, if you made your Main method async (ie: static async void Main() ), it is probably exiting before you ever see the exception.

     

    -Reed

     


    Reed Copsey, Jr. - http://reedcopsey.com
    If a post answers your question, please click "Mark As Answer" on that post and "Mark as Helpful".
    Tuesday, November 2, 2010 8:29 PM
    Moderator
  • re: "I suspect the problem is that, if you made your Main method async (ie: static async void Main() ), it is probably exiting before you ever see the exception."

    Exactly.  Remember that when you "await" something, you're really returning out of the call to the current method, and returning out of Main exits the application.  If you call asynchronous methods from Main, and if you care about those asynchronous methods completing before you exit your application, you'll need to synchronously block waiting for those tasks to complete (i.e. task.Wait()).

    Tuesday, November 2, 2010 10:24 PM
    Moderator
  • No, my Main method is not marked as async (see above) and it continues normally...

    I've tried Reed's suggested code and it executes correctly. But when I've modified Test method by eliminating try..catch block then I'm getting similar result as in my code - no exception is thrown. So this seems to me that OperationCanceledException is swallowed somewhere. Modified Test method is like this:

    static async void Test (CancellationTokenSource cts)
    {
      var task = CalculateAsync(cts.Token);
      int result = await task;
      Console.WriteLine(result);
    }
    
    
    Tuesday, November 2, 2010 10:37 PM
  • Hi Vlada-

    What does the call to Test look like?  I'm just trying to understand what the code looks like from the time your repro starts until the call to Test.  Is it possible for you to post the whole, short repro here so we can see it?

    Wednesday, November 3, 2010 4:35 AM
    Moderator
  • Scratch that... I just noticed your Test method is returning void.  When you return void from an async method, you're stating that this is fire-and-forget, that nothing is waiting for this operation to complete.  Thus, if you don't force your application to wait in some other way (i.e. blocking waiting for console input), your app will just complete.
    Wednesday, November 3, 2010 4:45 AM
    Moderator
  • I'm afraid this is not the cause. My console app Main method:

    static void Main (string[] args)
    {
      CancellationTokenSource cts = new CancellationTokenSource();
      Console.WriteLine("Starting Test Calculate from main thread");
      TestCalculate(cts.Token);
      Console.WriteLine("Test Calculate running - press 'c' to cancel it");
     if (Console.ReadKey().KeyChar == 'c')
       cts.Cancel();
      Console.ReadLine(); // waiting for result
    }
    
    

    So after pressing 'c' app is simply waiting in ReadLine() and no exception is thrown.

    One more observation: I've run it under debugger (with Just My Code enabled) and following happened:

    1. OperationCanceledException thrown at ct.ThrowIfCancellationRequested
    2. TaskCanceledException thrown at the end of TestCalculate method

    After pressing F5 it simply continues (i.e. waiting at ReadLine).

    Maybe this behaviour is intentional ? But then it should be described in documentation.

     

    Wednesday, November 3, 2010 12:20 PM
  • Hi Vlada-

    I see.  What's happening is that the OperationCanceledException is getting thrown out of async method, but because the exception is happening at a location where it was called back into as the result of an async operation completing, the exception is escaping into the continuation task used off of the awaited task.  That task captures that exception, and the exception gets eaten.  We'll investigate whether there's something better we could do here, but what do you expect to happen in this case?  Your method is returning void, and there's no current SynchronizationContext, which means there's no means by which to somehow hand that exception back to code you write that cares about the exception.  Primary options would be to eat it or crash.

    Wednesday, November 3, 2010 3:07 PM
    Moderator
  • Hi Stephen,

    thanks for your explenation and support. After your post I did some more experiments so before closing the question let me write some notes and I'd really appreciate your comments.

    1. I think that problem with "lost" TaskCanceledException has nothing to do with new language support. I've tried to write some code with similar semantics using "old style" (specifically ContinueWith) and got the same result.

    2. In WinForms (or generally GUI) apps the exception is sent to UI SynchronizationContext so that the behaviour is as I expected (I've actually tried it).

    3. It seems exception is not completelly lost. I've explicitly called GC at the end of console app and finalizer has thrown this exception.

    4. "options would be to eat it or crash" - really hard decision. From one point of view no exception should be swallowed (like it was with exceptions on background threads in .NET 1.1). On the other side it's not real exception - it's only the means used by cancellation infrastructure. Furthermore - with current behaviour exception will eventually crash server app (like WCF service) anyway because of finalizer.

    5. I don't argue about current behavioiur, I just think that it should be cleraly stated in documentation. Although my example may look stupid I could imagine some scenarious in server apps where some "fire and forget" operation might be issued (saving current state to file, computing and storing some statistics etc.) and later cancelled (e.g. due to heavy workload).

    Regards,

      Vlada

     

    Wednesday, November 3, 2010 11:00 PM
  • Thanks for the feedback.

    • Marked as answer by Vlada Bodecek Thursday, November 4, 2010 12:45 AM
    Wednesday, November 3, 2010 11:58 PM
    Moderator
  • Thanks, a helpful discussion. I reworked Reed's sample to get (I think) the desired result.

    using System;
    using System.Threading.Tasks;
    using System.Threading;
    
    namespace CSharpConsole
    { 
     class Program
     {
     static Task<int> CalculateAsync(CancellationToken token)
     {
      return Task.Factory.StartNew(() =>
      {
       for (int i = 0; i < 100; ++i)
       {
       token.ThrowIfCancellationRequested();
       Thread.Sleep(100);
       }
       return 42;
      });
     }
    
     static async Task Test(CancellationTokenSource cts)
     {
      var task = CalculateAsync(cts.Token);
      int result = await task;
    
      Console.WriteLine(result);
     }
    
     static void Main(string[] args)
     {
      try
      {
       var cts = new CancellationTokenSource();
       var tsk = Test(cts);
    
       cts.Cancel();
       tsk.Wait();
      }
      catch (AggregateException e)
      {
       Console.WriteLine("Canceled! {0}", e.Message);
       foreach (var ex in e.InnerExceptions)
       {
        Console.WriteLine("... {0}", ex.Message);
       }
      }  
      Console.WriteLine("Press any key to exit...");
      Console.ReadKey();
     }
     }
    }
    
    
    Saturday, December 4, 2010 1:00 AM