简体   繁体   中英

How to implement Task Async for a timer in C#?

I want a given operation to execute for a certain amount of time. When that time has expired, send another execution command.

StartDoingStuff();
System.Threading.Thread.Sleep(200);
StopDoingStuff();

Rather than have a sleep statement in there that's blocking the rest of the application, how can I write this using an Async/Task/Await in C#?

This issue was answered by Joe Hoag in the Parallel Team's blog in 2011: Crafting a Task.TimeoutAfter Method .

The solution uses a TaskCompletionSource and includes several optimizations (12% just by avoiding captures), handles cleanup and covers edge cases like calling TimeoutAfter when the target task has already completed, passing invalid timeouts etc.

The beauty of Task.TimeoutAfter is that it is very easy to compose it with other continuations becaused it does only a single thing: notifies you that the timeout has expired. It doesnt' try to cancel your task. You get to decide what to do when a TimeoutException is thrown.

A quick implementation using async/await by Stephen Toub is also presented, although edge cases aren't covered as well.

The optimized implementation is:

public static Task TimeoutAfter(this Task task, int millisecondsTimeout)
{
    // Short-circuit #1: infinite timeout or task already completed
    if (task.IsCompleted || (millisecondsTimeout == Timeout.Infinite))
    {
        // Either the task has already completed or timeout will never occur.
        // No proxy necessary.
        return task;
    }

    // tcs.Task will be returned as a proxy to the caller
    TaskCompletionSource<VoidTypeStruct> tcs = 
        new TaskCompletionSource<VoidTypeStruct>();

    // Short-circuit #2: zero timeout
    if (millisecondsTimeout == 0)
    {
        // We've already timed out.
        tcs.SetException(new TimeoutException());
        return tcs.Task;
    }

    // Set up a timer to complete after the specified timeout period
    Timer timer = new Timer(state => 
    {
        // Recover your state information
        var myTcs = (TaskCompletionSource<VoidTypeStruct>)state;

        // Fault our proxy with a TimeoutException
        myTcs.TrySetException(new TimeoutException()); 
    }, tcs, millisecondsTimeout, Timeout.Infinite);

    // Wire up the logic for what happens when source task completes
    task.ContinueWith((antecedent, state) =>
    {
        // Recover our state data
        var tuple = 
            (Tuple<Timer, TaskCompletionSource<VoidTypeStruct>>)state;

        // Cancel the Timer
        tuple.Item1.Dispose();

        // Marshal results to proxy
        MarshalTaskResults(antecedent, tuple.Item2);
    }, 
    Tuple.Create(timer, tcs),
    CancellationToken.None,
    TaskContinuationOptions.ExecuteSynchronously,
    TaskScheduler.Default);

    return tcs.Task;
}

and Stephen Toub's implementation, without checks for edge cases :

public static async Task TimeoutAfter(this Task task, int millisecondsTimeout)
{
    if (task == await Task.WhenAny(task, Task.Delay(millisecondsTimeout))) 
        await task;
    else
        throw new TimeoutException();
}

Assuming StartDoingStuff and StopDoingStuff have been created as Async methods returning Task then

await StartDoingStuff();
await Task.Delay(200);
await StopDoingStuff();

EDIT: If the original questioner wants an asynchronous method that will cancel after a specific period: assuming the method would not be making any network requests but just be doing some processing in memory and the outcome can be aborted arbitrarily without considering its effects, then use a cancellation token:

    private async Task Go()
    {
        CancellationTokenSource source = new CancellationTokenSource();
        source.CancelAfter(200);
        await Task.Run(() => DoIt(source.Token));

    }

    private void DoIt(CancellationToken token)
    {
        while (true)
        {
            token.ThrowIfCancellationRequested();
        }
    }

EDIT : I should have mentioned you can catch the resulting OperationCanceledException providing the indication on how the Task ended, avoiding the need to mess around with bools.

Here's how I'd do it, using task cancellation pattern (the option without throwing an exception).

[EDITED] Updated to use Svick's suggestion to set the timeout via CancellationTokenSource constructor .

// return true if the job has been done, false if cancelled
async Task<bool> DoSomethingWithTimeoutAsync(int timeout) 
{
    var tokenSource = new CancellationTokenSource(timeout);
    CancellationToken ct = tokenSource.Token;

    var doSomethingTask = Task<bool>.Factory.StartNew(() =>
    {
        Int64 c = 0; // count cycles

        bool moreToDo = true;
        while (moreToDo)
        {
            if (ct.IsCancellationRequested)
                return false;

            // Do some useful work here: counting
            Debug.WriteLine(c++);
            if (c > 100000)
                moreToDo = false; // done counting 
        }
        return true;
    }, tokenSource.Token);

    return await doSomethingTask;
}

Here's how to call it from an async method:

private async void Form1_Load(object sender, EventArgs e)
{
    bool result = await DoSomethingWithTimeoutAsync(3000);
    MessageBox.Show("DoSomethingWithTimeout done:" + result); // false if cancelled
}

Here's how to call it from a regular method and handle the completion asynchronously:

private void Form1_Load(object sender, EventArgs e)
{
    Task<bool> task = DoSomethingWithTimeoutAsync(3000);
    task.ContinueWith(_ =>
    {
        MessageBox.Show("DoSomethingWithTimeout done:" + task.Result); // false is cancelled
    }, TaskScheduler.FromCurrentSynchronizationContext());
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM