hannasm
hannasm

Reputation: 2041

Async Lazy Timeout Task

I have an async operation dependent on another server which takes a mostly random amount of time to complete. While the async operation is running there is also processing going on in the 'main thread' which also takes a random amount of time to complete.

The main thread starts the asynchronous task, executes it's primary task, and checks for the result of the asynchronous task at the end.

The async thread pulls data and computes fields which are not critical for the main thread to complete. However this data would be nice to have (and should be included) if the computation is able to complete without slowing down the main thread.

I'd like to setup the async task to run at minimum for 2 seconds, but to take all the time available between start and end of the main task. It's a 'lazy timeout' in that it only timeouts if exceeded the 2 second runtime and the result is actually being requested. (The async task should take the greater of 2 seconds, or the total runtime of the main task)

EDIT (trying to clarify the requirements): If the async task has had a chance to run for 2 seconds, it shouldn't block the main thread at all. The main thread must allow the async task at least 2 seconds to run. Furthermore, if the main thread takes more than 2 seconds to complete, the async task should be allowed to run as long as the main thread.

I've devised a wrapper that works, however i'd prefer a solution that is actually of type Task. See my wrapper solution below.

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

Does anyone have a better solution than this wrapper?

Upvotes: 4

Views: 1372

Answers (2)

svick
svick

Reputation: 244757

I don't think you can make Task<T> behave this way, because Result is not virtual and there also isn't any other way to change its behavior.

I also think you shouldn't even try to do this. The contract of the Result property is to wait for the result (if it's not available yet) and return it. It's not to cancel the task. Doing that would be very confusing. If you're cancelling the task, I think it should be obvious from the code that you're doing it.

If I were to do this, I would create a wrapper for the Task<T>, but it would look like this:

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

Note that the computation has a CancellationToken parameter. That's because you can't force cancellation (without dirty tricks like Thread.Abort()) and the computation has to explicitly support it, ideally by executing cancellationToken.ThrowIfCancellationRequested() at appropriate times.

Upvotes: 1

usr
usr

Reputation: 171178

I'd always start a 2 second task that, when it completes, marks your computation as cancelled . This saves you the strange "diff" time calculation. Here is some code:

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;

This is pseudo-code. I only wanted to show the technique.

TryCopyResultFrom is meant to copy the computation.Result to the TaskCompletionSource tcs by calling TrySetResult().

Your app just uses taskToWaitOn. It will transition to cancelled after 2s. If the computation completes earlier, it will receive the result of that.

Upvotes: 1

Related Questions