3

Like many others, I need to write a function that returns a task, and I want that task to automatically time out after a certain period.

The initial code looks like this:

class MyClass
{
    TaskCompletionSource<string> m_source;

    public Task<string> GetDataFromServer()
    {
        m_source = new TaskCompletionSource<string> ();

        // System call I have no visibility into, and that doesn't inherently take any
        // sort of timeout or cancellation token
        ask_server_for_data_and_when_youve_got_it_call(Callback);

        return m_source.Task;
    }

    protected void Callback(string data);
    {
        // Got the data!
        m_source.TrySetResult(data);
    }
}

Now I want this to be a little smarter, and time itself out when appropriate. I have several options for doing this:

class MyClass
{
    TaskCompletionSource<string> m_source;

    public Task<string> GetDataFromServer(int timeoutInSeconds)
    {
        m_source = new TaskCompletionSource<string> ();

        ask_server_for_data_and_when_youve_got_it_call(Callback);

        // Method #1 to set up the timeout:
        CancellationToken ct = new CancellationToken ();
        CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource (ct);
        cts.CancelAfter (timeoutInSeconds * 1000);
        cts.Token.Register(() => m_source.TrySetCancelled());

        // Method #2 to set up the timeout:
        CancellationTokenSource ct2 = new CancellationTokenSource ();
        ct2.CancelAfter (timeoutInSeconds * 1000);
        ct2.Token.Register (() => m_source.TrySetCancelled());

        // Method #3 to set up the timeout:
        System.Threading.Tasks.Task.Factory.StartNew (async () =>
        {
            await System.Threading.Tasks.Task.Delay (timeoutInSeconds * 1000);
            m_source.TrySetCancelled();
        });

        // Method #4 to set up the timeout:
        Xamarin.Forms.Device.StartTimer (new TimeSpan (0, 0, timeoutInSeconds),
            () => m_source.TrySetCancelled());

        return m_source.Task;
    }

    protected void Callback(string data);
    {
        // Got the data!
        m_source.TrySetResult(data);
    }
}

What are the plusses and minuses to the 4 different ways of setting up a timeout? For instance, I'm guessing that method #2 is the most "lightweight" (requiring the fewest system resources)?

Are there other ways to set up a timeout that I've missed?

p.s.

One piece of knowledge I found out the hard way - if you call GetDataFromServer() from a thread besides the main UI thread:

Task.Run(() => await GetDataFromServer());    

On iOS, the fourth method (Xamarin.Forms.Device.StartTimer) never fires

Betty Crokker
  • 3,001
  • 6
  • 34
  • 68
  • The first function is async, so it should return the string, and not the Task. If you want this function to return the task itself, remove the `async` keyword – Peter Bruins Mar 14 '17 at 17:56
  • I've added the 'await' function that is why I have the 'async' keyword. – Betty Crokker Mar 14 '17 at 18:01
  • But this doesn't even compile... It looks like your are mixing synchronous and asynchronous code. First you send a async request to the server, and then a synchronous request with a callback. A function with `public async Task......` should return a string – Peter Bruins Mar 14 '17 at 18:08
  • You are right, this is not my actual code, but just sample code to help explain my question. I've updated the code to remove the 'async'. – Betty Crokker Mar 14 '17 at 19:43

1 Answers1

4

I think it's easier to just use Task.Delay and Task.WhenAny:

public async Task<string> GetDataFromServerAsync(int timeoutInSeconds)
{
  Task<string> requestTask = GetDataFromServerAsync();
  var timeoutTask = Task.Delay(timeoutInSeconds);
  var completedTask = await Task.WhenAny(requestTask, timeoutTask);
  if (completedTask == timeoutTask)
    throw new OperationCanceledException();
  return await requestTask;
}

Cons of the other approaches:

Method #1: Creates a new CancellationToken for no reason. It's just a less-efficient version of Method #2.

Method #2: Normally, you should be disposing the result of Register once the task completes. In this case, it would probably work OK since the CTS is always eventually cancelled.

Method #3: Uses StartNew just to call Delay - not sure of the reasoning there. It's essentially a less-efficient version of Delay with WhenAny.

Method #4: Would be acceptable. Though you do have to deal with TaskCompletionSource<T> and its quirks (e.g., synchronous continuations by default).

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810