Async Lazy Timeout Task - PullRequest
       14

Async Lazy Timeout Task

4 голосов
/ 04 февраля 2012

У меня есть асинхронная операция, зависящая от другого сервера, которая занимает в основном случайное количество времени.Во время выполнения асинхронной операции в «главном потоке» также происходит обработка, для завершения которой также требуется произвольное время.

Основной поток запускает асинхронную задачу, выполняет свою основную задачу и проверяет еедля результата асинхронной задачи в конце.

Асинхронный поток извлекает данные и вычисляет поля, которые не являются критическими для завершения основного потока.Однако эти данные было бы неплохо иметь (и должны быть включены), если вычисление может быть завершено без замедления основного потока.

Я бы хотел настроитьасинхронная задача выполняется минимум 2 секунды, но занимает все доступное время между началом и концом основной задачи.Это «ленивый тайм-аут» в том смысле, что он только тайм-ауты, если превышено 2-секундное время выполнения, и результат фактически запрашивается.(Асинхронная задача должна занимать более 2 секунд или общее время выполнения основной задачи)

РЕДАКТИРОВАТЬ (пытаясь уточнить требования): Если асинхронная задача имеетбыл шанс запустить в течение 2 секунд, он не должен блокировать основной поток вообще.Основной поток должен разрешить выполнение асинхронной задачи не менее 2 секунд.Кроме того, если основному потоку требуется более 2 секунд, асинхронная задача должна выполняться до тех пор, пока основной поток.

Я разработал оболочку, которая работает, однако я бы предпочелрешение, которое на самом деле имеет тип задачи.См. Мое решение для оболочки ниже.

public class LazyTimeoutTaskWrapper<tResult>
{
    private int _timeout;
    private DateTime _startTime;
    private Task<tResult> _task;
    private IEnumerable<Action> _timeoutActions;

    public LazyTimeoutTaskWrapper(Task<tResult> theTask, int timeoutInMillis, System.DateTime whenStarted, IEnumerable<Action> onTimeouts)
    {
        this._task = theTask;
        this._timeout = timeoutInMillis;
        this._startTime = whenStarted;
        this._timeoutActions = onTimeouts;
    }

    private void onTimeout()
    {
        foreach (var timeoutAction in _timeoutActions)
        {
            timeoutAction();
        }
    }

    public tResult Result
    {
        get
        {
            var dif = this._timeout - (int)System.DateTime.Now.Subtract(this._startTime).TotalMilliseconds;
            if (_task.IsCompleted ||
                (dif > 0 && _task.Wait(dif)))
            {
                return _task.Result;
            }
            else
            {
                onTimeout();
                throw new TimeoutException("Timeout Waiting For Task To Complete");
            }
        }
    }

    public LazyTimeoutTaskWrapper<tNewResult> ContinueWith<tNewResult>(Func<Task<tResult>, tNewResult> continuation, params Action[] onTimeouts)
    {
        var result = new LazyTimeoutTaskWrapper<tNewResult>(this._task.ContinueWith(continuation), this._timeout, this._startTime, this._timeoutActions.Concat(onTimeouts));
        result._startTime = this._startTime;
        return result;
    }
}

У кого-нибудь есть лучшее решение, чем эта оболочка?

Ответы [ 2 ]

1 голос
/ 04 февраля 2012

Я не думаю, что вы можете заставить Task<T> вести себя таким образом, потому что Result не является virtual и нет другого способа изменить его поведение.

Я также думаю, что вы даже не должны пытаться это делать. Контракт свойства Result заключается в том, чтобы дождаться результата (если он еще не доступен) и вернуть его. Это не отменить задачу. Делать это было бы очень запутанным. Если вы отменяете задачу, я думаю, из кода должно быть очевидно, что вы ее выполняете.

Если бы я сделал это, я бы создал оболочку для Task<T>, но это выглядело бы так:

class CancellableTask<T>
{
    private readonly Func<CancellationToken, T> m_computation;
    private readonly TimeSpan m_minumumRunningTime;

    private CancellationTokenSource m_cts;
    private Task<T> m_task;
    private DateTime m_startTime;

    public CancellableTask(Func<CancellationToken, T> computation, TimeSpan minumumRunningTime)
    {
        m_computation = computation;
        m_minumumRunningTime = minumumRunningTime;
    }

    public void Start()
    {
        m_cts = new CancellationTokenSource();
        m_task = Task.Factory.StartNew(() => m_computation(m_cts.Token), m_cts.Token);
        m_startTime = DateTime.UtcNow;
    }

    public T Result
    {
        get { return m_task.Result; }
    }

    public void CancelOrWait()
    {
        if (m_task.IsCompleted)
            return;

        TimeSpan remainingTime = m_minumumRunningTime - (DateTime.UtcNow - m_startTime);

        if (remainingTime <= TimeSpan.Zero)
            m_cts.Cancel();
        else
        {
            Console.WriteLine("Waiting for {0} ms.", remainingTime.TotalMilliseconds);
            bool finished = m_task.Wait(remainingTime);
            if (!finished)
                m_cts.Cancel();
        }
    }
}

Обратите внимание, что вычисление имеет параметр CancellationToken. Это потому, что вы не можете форсировать отмену (без грязных трюков, таких как Thread.Abort()), и вычисления должны явно поддерживать это, в идеале, выполняя cancellationToken.ThrowIfCancellationRequested() в соответствующее время.

1 голос
/ 04 февраля 2012

Я всегда начинаю двухсекундное задание, которое после его завершения помечает ваше вычисление как отмененное. Это спасает вас от странного вычисления времени "diff". Вот некоторый код:

Task mainTask = ...; //represents your main "thread"
Task computation = ...; //your main task
Task timeout = TaskEx.Delay(2000);

TaskCompletionSource tcs = new TCS();

TaskEx.WhenAll(timeout, mainTask).ContinueWith(() => tcs.TrySetCancelled());
computation.ContinueWith(() => tcs.TryCopyResultFrom(computation));

Task taskToWaitOn = tcs.Task;

Это псевдокод. Я только хотел показать технику.

TryCopyResultFrom предназначен для копирования вычислений. Результат для tcs TaskCompletionSource с помощью вызова TrySetResult ().

Ваше приложение просто использует taskToWaitOn. Он перейдет в отменено через 2 с. Если вычисление завершится раньше, он получит результат этого.

...