1

Is there a way to wait for a task to end but move on after a certain period of time but let the task itself continue on? I would like a response back from a task within 5 seconds and don't want to wait longer than that -- however, if longer than 5 seconds have passed, I just want to move on and not actually cancel the task (ruling out cancellation tokens).

I'm using .NET Core / C#.

private async Task<string> MakeAuthRequest(Request request)
{
    try
    {
        var response = await SometimesLongMethod(request); // something needs to be done here, just not sure what
        return response.Message;
    }
    catch(Exception e)
    {
        logger.Error(e, "Error occurred");
        return string.Empty;
    }
}
jollyroger23
  • 665
  • 1
  • 6
  • 19
  • 3
    `Task.Delay` + `Task.WaitAny`? – Sinatr Jun 11 '19 at 14:29
  • So what happens with your return if you stop waiting and let the task continue in the background? Return `default(string)` perhaps? – Jamiec Jun 11 '19 at 14:29
  • Im lost here, how you plan to return something if the task is yet to end? – nalnpir Jun 11 '19 at 14:30
  • [`Task.Wait(TimeSpan)`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.wait?view=netframework-4.8#System_Threading_Tasks_Task_Wait_System_TimeSpan_) and [`Task.Wait(Int32)`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.wait?view=netframework-4.8#System_Threading_Tasks_Task_Wait_System_Int32_) both allow you to specify a maximum amount of time to wait. – Rufus L Jun 11 '19 at 14:35

3 Answers3

1

You can use Task.Delay to have a task representing a span of time, and then use Task.WhenAny to determine which task completes first:

private async Task<string> MakeAuthRequest(Request request)
{
  try
  {
    var timer = Task.Delay(TimeSpan.FromSeconds(5));
    var authTask = SometimesLongMethod(request);
    var completedTask = await Task.WhenAny(timer, authTask);
    if (completedTask == authTask)
      return await authTask;
    return string.Empty; // Did not complete in 5 seconds.
  }
  catch(Exception e)
  {
    logger.Error(e, "Error occurred");
    return string.Empty;
  }
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • You should always cancel the delay task if your task comes back. Or you get dangling timers. – Jamiec Jun 11 '19 at 14:41
  • I think the OP wanted the MakeAuthRequest task to continue even if the timeout condition was met, your snippet above wouldn't do that It needs to be the next level up the Task chain. – JoeTomks Jun 11 '19 at 14:41
  • @Jamiec: The delay task will just complete later. While you could cancel it, I don't see it making a big difference. – Stephen Cleary Jun 11 '19 at 14:42
  • 1
    @Digitalsa1nt: I interpreted the OP's request as wanting to do the timeout where they had the `// something needs to be done here` placeholder. – Stephen Cleary Jun 11 '19 at 14:54
1

Something like this will do - remember to cancel the delay task if your method returns quickly!

 public static async Task<T> OnlyWaitFor<T>(Task<T> theTask, TimeSpan waitTime)
 {
    var cts = new CancellationTokenSource();
    var delayTask = Task.Delay(waitTime,cts.Token);
    var result = await Task.WhenAny(delayTask, theTask);
    if (result == delayTask)
        return default(T);
    cts.Cancel();
    return await theTask;

}

Test code:

 public static async Task<string> ShortTask()
 {
     await Task.Delay(500);
     return "ShortFoo";
 }

 public static async Task<string> LongTask()
 {
     await Task.Delay(5000);
     return "LongFoo";
 }

var sw = new Stopwatch();
sw.Start();
var result1 = await OnlyWaitFor(ShortTask(), TimeSpan.FromSeconds(1));
sw.Stop();
Console.WriteLine($"Got back '{result1}' in {sw.ElapsedMilliseconds}ms");

sw.Reset();
sw.Start();
var result2 = await OnlyWaitFor(LongTask(), TimeSpan.FromSeconds(1));
sw.Stop();
Console.WriteLine($"Got back '{result2}' in {sw.ElapsedMilliseconds}ms");

Output:

Got back 'ShortFoo' in 524ms
Got back '' in 1006ms
Jamiec
  • 133,658
  • 13
  • 134
  • 193
-1

The extension methods below allow setting a timeout to the awaiting of any task. The task itself will continue running. The result of a timed-out Task<T> will be the default value of T. For example a timed-out Task<int> results to the value 0.

public static Task WithTimeout(this Task task, int timeout)
{
    var delayTask = Task.Delay(timeout);
    return Task.WhenAny(task, delayTask).Unwrap();
}

public static Task<T> WithTimeout<T>(this Task<T> task, int timeout)
{
    var delayTask = Task.Delay(timeout).ContinueWith(_ => default(T),
        TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, delayTask).Unwrap();
}

A usage example can be found in this answer.

If the default value of T is a valid return value, you may prefer to handle an exception instead:

public static Task<T> WithTimeoutException<T>(this Task<T> task, int timeout)
{
    var delayTask = Task.Delay(timeout).ContinueWith<T>(_ => throw new TimeoutException(),
        TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, delayTask).Unwrap();
}

Update: In case you change your mind and prefer to cancel the timed-out tasks, see this question: Task.WhenAny with cancellation of the non completed tasks and timeout.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104