Асинхронная задача отложенного тайм-аута

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

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

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

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

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

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

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

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


person hannasm    schedule 04.02.2012    source источник
comment
Мне любопытно, почему вы когда-либо хотели, чтобы фоновая задача занимала больше времени, чем требуется для выполнения фактической работы.   -  person Chris Shain    schedule 04.02.2012
comment
Дело не в том, что это должно занимать больше времени, чем реальная работа. Основной поток хочет подождать не более 2 секунд, но основной поток хочет разрешить ему работать более 2 секунд, если основной поток слишком занят, чтобы получить результат сразу. Основной поток вообще не хочет ждать, если основной поток занял более 2 секунд.   -  person hannasm    schedule 04.02.2012


Ответы (2)


Я всегда запускаю 2-секундную задачу, которая по завершении помечает ваши вычисления как отмененные. Это избавит вас от странного расчета времени "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 предназначен для копирования вычислений.Result в tcs TaskCompletionSource путем вызова TrySetResult().

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

person usr    schedule 04.02.2012
comment
Я никогда не видел TryCopyResultFrom() и, видимо, Google тоже. Откуда это? Уж точно не из .Net (будь то 4.0 или 4.5). - person svick; 04.02.2012
comment
Кроме того, я думаю, что это не то, о чем спрашивают. Вычисление должно быть отменено через 2 секунды, только если основной поток уже завершен. - person svick; 04.02.2012
comment
второй svick должен ждать минимум 2 секунды и 100 секунд, если основной поток занимает 100 секунд. - person hannasm; 04.02.2012
comment
Я обновил код. Теперь он ожидает истечения времени ожидания и mainTask. Я также пояснил, что означает TryCopyResultFrom. Помните, что это псевдокод. Вы должны сделать эту работу самостоятельно. Я просто хочу дать вам представление о том, как это может работать. - person usr; 04.02.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() в соответствующее время.

person svick    schedule 04.02.2012
comment
Смотрите мое верхнее редактирование, как бы вы предпочли реализовать эти требования? - person hannasm; 04.02.2012