Время ожидания асинхронного метода, реализованного с помощью TaskCompletionSource

У меня есть объект черного ящика, который предоставляет метод для запуска асинхронной операции, и событие срабатывает, когда операция завершена. Я обернул это в метод Task<OpResult> BlackBoxOperationAysnc(), используя TaskCompletionSource — это работает хорошо.

Однако в этой асинхронной оболочке я хотел бы управлять завершением асинхронного вызова с ошибкой тайм-аута, если событие не получено после заданного тайм-аута. В настоящее время я управляю им с помощью таймера:

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;
}

Это единственный способ управлять тайм-аутом? Есть ли способ без настройки моего собственного таймера - я не мог видеть тайм-аут, встроенный в TaskCompletionSource?


person Ricibob    schedule 12.09.2013    source источник


Ответы (2)


Вы можете использовать CancellationTokenSource с тайм-аутом. Используйте его вместе с вашим TaskCompletionSource, например этим.

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;
}

Обновлено, вот полный пример работы:

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();
        }
    }
}

Версию .NET 4.0/C# 4.0 можно найти здесь. IEnumerator конечный автомат.

person noseratio    schedule 12.09.2013
comment
Спасибо. Примечание. CancellationTokenSource имеет конструктор тайм-аута только в .NET4.5 и выше (не в .NET4.0). - person Ricibob; 12.09.2013
comment
Мне кажется, вы используете CancellationTokenSource вместо того, чтобы просто запускать отложенную задачу. Я бы предпочел Task.Factory.StartNewDelayed(20000, () => tcs.TrySetCanceled()) для удобочитаемости в .NET 4.5. - person angularsen; 01.04.2014
comment
@AndreasLarsen, для этого потребуется ParallelExtensionsExtras, но не стесняйтесь публиковать свой собственный ответ, если хотите. - person noseratio; 01.04.2014
comment
@Noseratio, хорошая мысль. Я не осознавал, что использую для этого сторонний код. - person angularsen; 02.04.2014
comment
Этот код фактически помог мне добавить функциональность TimeOut к вызову WebAPi с использованием TaskCompletionSource. Спасибо! Я на самом деле не хотел отменять задачу, а выбрасывал исключение. Поэтому я добавил TrySetException после cts.Dispose внутри метода Register. Я также обнаружил, что мне нужно избавиться от CancellationTokenSource перед вызовом TrySetResult в моем TaskCompletionSource (используя метод Dispose для CancellationTokenSource), иначе токен отмены останется и в конечном итоге истечет время ожидания! - person mike gold; 20.08.2015
comment
это гениально! Мне пришлось использовать версию .NET 4.0, потому что мы все еще застряли в 4.0 с проектом, в котором у нас возникла проблема с управляемым поставщиком данных Oracle игнорирует свойство CommandTimeout, поэтому нам пришлось свернуть собственное, и это помогло. есть одно небольшое исправление, ваша строка кода 58 должна быть this.StartBlackBoxOperation(state); вместо this.StartBlackBoxOperation(null);, поэтому рабочие элементы, которые фактически используют ненулевой объект состояния, получат его. - person Cee McSharpface; 09.09.2016
comment
Спасибо за этот код! Однако мне пришлось заменить все tcs.Set* на tcs.TrySet, потому что было много случаев состояния гонки, когда tcs уже был установлен в конечное состояние, что приводило к InvalidOperationException - person Wizou; 19.09.2016
comment
Поскольку CancellationTokenSource равно IDisposable, вы должны реализовать using или вызвать Dispose - person Alexandre; 13.02.2019
comment
Более общее решение здесь: stackoverflow.com/a/25987969/2440 - person Sire; 29.05.2019

Вы можете использовать расширение для Task отсюда (https://stackoverflow.com/a/22078975/2680660), которое также использует CancellationTokenSource.

С небольшой модификацией:

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    using (var timeoutCancellationTokenSource = new CancellationTokenSource())
    {
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == task)
        {
            timeoutCancellationTokenSource.Cancel();
            return await task;  // Very important in order to propagate exceptions
        }
        else
        {
            throw new TimeoutException($"{nameof(TimeoutAfter)}: The operation has timed out after {timeout:mm\\:ss}");
        }
    }
}

public Task<OpResult> BlackBoxOperationAysnc()
{
    var tcs = new TaskCompletionSource<TestResult>();   

    EventHandler<EndOpEventArgs> eventHandler = (sender, args) => {
        ...
        tcs.TrySetResult(OpResult.BlarBlar);
    }
    blackBox.EndAsyncOpEvent += eventHandler;
    blackBox.StartAsyncOp();
    return tcs.Task.TimeoutAfter(TimeSpan.FromSeconds(20));
}
person Efreeto    schedule 04.05.2020
comment
Разве это не склонно к UnobservedTaskException из-за отмены тайм-аута Task после завершения другой задачи, поэтому никто никогда не ждет результата? - person AyCe; 26.03.2021