Search code examples
c#asynchronoustimeoutasync-awaittaskcompletionsource

Timeout an async method implemented with TaskCompletionSource


I have a blackbox object that exposes a method to kick of an async operation, and an event fires when the operation is complete. I have wrapped that into an Task<OpResult> BlackBoxOperationAysnc() method using TaskCompletionSource - that works well.

However, in that async wrapper I'd like to manage completing the async call with a timeout error if the event is not received after a given timeout. Currently I manage it with a timer as:

public Task<OpResult> BlackBoxOperationAysnc() {
    var tcs = new TaskCompletionSource<TestResult>();   
    const int timeoutMs = 20000;
    Timer timer = new Timer(_ => tcs.TrySetResult(OpResult.Timeout),
                            null, timeoutMs, Timeout.Infinite);

    EventHandler<EndOpEventArgs> eventHandler = (sender, args) => {
        ...
        tcs.TrySetResult(OpResult.BlarBlar);
    }
    blackBox.EndAsyncOpEvent += eventHandler;
    blackBox.StartAsyncOp();
    return tcs.Task;
}

Is that the only way to manage a timeout? Is there someway without setting up my own timer - I couldn't see anything timeout built into TaskCompletionSource?


Solution

  • You could use CancellationTokenSource with timeout. Use it together with your TaskCompletionSource like this.

    E.g.:

    public Task<OpResult> BlackBoxOperationAysnc() {
        var tcs = new TaskCompletionSource<TestResult>();
    
        const int timeoutMs = 20000;
        var ct = new CancellationTokenSource(timeoutMs);
        ct.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false);
    
        EventHandler<EndOpEventArgs> eventHandler = (sender, args) => {
            ...
            tcs.TrySetResult(OpResult.BlarBlar);
        }
        blackBox.EndAsyncOpEvent += eventHandler;
        blackBox.StartAsyncOp();
        return tcs.Task;
    }
    

    Updated, here's a complete functional example:

    using System;
    using System.ComponentModel;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication
    {
        public class Program
        {
            // .NET 4.5/C# 5.0: convert EAP pattern into TAP pattern with timeout
            public async Task<AsyncCompletedEventArgs> BlackBoxOperationAsync(
                object state,
                CancellationToken token,
                int timeout = Timeout.Infinite)
            {
                var tcs = new TaskCompletionSource<AsyncCompletedEventArgs>();
                using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token))
                {
                    // prepare the timeout
                    if (timeout != Timeout.Infinite)
                    {
                        cts.CancelAfter(timeout);
                    }
    
                    // handle completion
                    AsyncCompletedEventHandler handler = (sender, args) =>
                    {
                        if (args.Cancelled)
                            tcs.TrySetCanceled();
                        else if (args.Error != null)
                            tcs.SetException(args.Error);
                        else
                            tcs.SetResult(args);
                    };
    
                    this.BlackBoxOperationCompleted += handler;
                    try
                    {
                        using (cts.Token.Register(() => tcs.SetCanceled(), useSynchronizationContext: false))
                        {
                            this.StartBlackBoxOperation(null);
                            return await tcs.Task.ConfigureAwait(continueOnCapturedContext: false);
                        }
                    }
                    finally
                    {
                        this.BlackBoxOperationCompleted -= handler;
                    }
                }
            }
    
            // emulate async operation
            AsyncCompletedEventHandler BlackBoxOperationCompleted = delegate { };
    
            void StartBlackBoxOperation(object state)
            {
                ThreadPool.QueueUserWorkItem(s =>
                {
                    Thread.Sleep(1000);
                    this.BlackBoxOperationCompleted(this, new AsyncCompletedEventArgs(error: null, cancelled: false, userState: state));
                }, state);
            }
    
            // test
            static void Main()
            {
                try
                {
                    new Program().BlackBoxOperationAsync(null, CancellationToken.None, 1200).Wait();
                    Console.WriteLine("Completed.");
                    new Program().BlackBoxOperationAsync(null, CancellationToken.None, 900).Wait();
                }
                catch (Exception ex)
                {
                    while (ex is AggregateException)
                        ex = ex.InnerException;
                    Console.WriteLine(ex.Message);
                }
                Console.ReadLine();
            }
        }
    }
    

    A .NET 4.0/C# 4.0 vesion can be found here, it takes advantage of the compiler-generated IEnumerator state machine.