1

Given code similar to

Task.Run(() =>
{
    using (var client = new HttpClient())
    {
        var responseTask = client.GetAsync(urlToInvoke);
    }
});

In a situation like this, it appears that GetAsync does not actually operate. Is the task canceled prior to completion or what is actually going on here?

Now if you change things slightly and insert

Task.Run(() =>
{
    using (var client = new HttpClient())
    {
        var responseTask = client.GetAsync(urlToInvoke);

        Task.Delay(5000).Wait()
    }
});

GetAsync does execute completely. What is going on here? Is Task.Delay affinitizing itself to the same Task that is inside responseTask ultimately making this equivalent to responseTask.Wait()?

Chris Marisic
  • 32,487
  • 24
  • 164
  • 258
  • nothing happens... you just got a task object and it never gets executed. – CrazyDart Aug 21 '14 at 16:29
  • @CrazyDart then why does it "work fine" with the inclusion of `Task.Delay`? – Chris Marisic Aug 21 '14 at 16:32
  • If a not awaited task finishes with an exception, the application [may crash](http://stackoverflow.com/q/16093846/276994). When you await the task, you get the exception delivered to the place of `await`. – Vlad Aug 21 '14 at 16:32
  • I would suspect its not the Delay that causes it to work... its the Wait. On that task object if you access result or call wait it will execute. – CrazyDart Aug 21 '14 at 16:35

2 Answers2

4

When you don't await (or Wait) tasks they do not cancel themselves. They continue to run until they reach one of three statuses:

  • RanToCompletion - Finished successfully.
  • Canceled - The cancellation token was canceled.
  • Faulted - An unhandled exception occurred inside the task.

In your case however, because no one waits for the task to complete, the using scope ends which disposes of the HttpClient. This in turn will cancel all the client's tasks, client.GetAsync(urlToInvoke) in this case. So that inner async task will end immediately and become Canceled, while the outer task (Task.Run) will simply end without doing anything.

When you use Task.Delay(5000).Wait() which is basically Thread.Sleep(5000) the task has a chance to complete before the using scope ends. That mode of operation however should be avoided. It blocks a thread throughout the Waitand could lead to deadlocks in single threaded SynchronizationContexts. This also hides possible exceptions in the task (which could tear down the application in earlier versions of .Net)

You should always wait for tasks to complete, preferably asynchronously, and as Servy commented, there's no reason to use Task.Run here for offloading because GetAsyncis asynchronous and won't block the calling thread.

using (var client = new HttpClient())
{
    var response = await client.GetAsync(urlToInvoke);
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • There's also no need to be using `Task.Run` here, since the method is itself asynchronous, and won't be blocking the current thread. – Servy Aug 21 '14 at 17:33
  • @Servy I don't want the Worker thread blocking at all that's why it's in the `Task.Run` statement. Otherwise I would pay the wait time on the original thread, in my case the first request to the site making wake up time even greater. – Chris Marisic Aug 21 '14 at 19:19
  • 1
    @ChrisMarisic But the code is already asynchronous. Starting an asynchronous operation in another thread is pointless. It already won't block the current thread. That's what it means to be asynchronous. – Servy Aug 21 '14 at 19:20
  • `Thread 1 { async stuff, takes 10 seconds } WaitAll()` Thread 1 still takes 10 seconds. `Thread 1 { Thread 2 { async stuff, takes 10 seconds } WaitAll() } }` Thread 2 takes 10 seconds, Thread 1 takes 0. Unless we're falling into pedantics, in Scenario 1 being an IIS worker thread, the client (web browser) is certainly blocked the full 10 seconds. Scenario 2 the client is not blocked at all. – Chris Marisic Aug 21 '14 at 19:23
4

You are thinking of it incorrectly. Here is pseudo version of what is happening inside the class.

class HttpClient : IDisposeable
{
    private CancelationTokenSource _disposeCts;

    public HttpClient()
    {
        _disposeCts = new CancelationTokenSource();
    }

    public Task<HttpResponseMessage> GetAsync(string url)
    {
        return GetAsync(url, CancellationToken.None);
    }

    public async Task<HttpResponseMessage> GetAsync(string url, CancelationToken token)
    {
        var combinedCts =
            CancellationTokenSource.CreateLinkedTokenSource(token, _disposeCts.Token);
        var tokenToUse = combinedCts.Token;

        //... snipped code

        //Some spot where it would good to check if we have canceled yet.
        tokenToUse.ThrowIfCancellationRequested();

        //... More snipped code;

        return result;
    }

    public void Dispose()
    {
        _disposeCts.Cancel();
    }

    //... A whole bunch of other stuff.
}

The important thing to see is when you exit the using block a internal cancelation token is canceled.

In your first example the task had not finished yet so tokenToUse would now throw if ThrowIfCancellationRequested() was called.

In your second example the task had already finished so the act of canceling the internal token had no effect on the task that was returned due to it already reaching the completed state.

It is like asking why this causes the task to be canceled.

using (var client = new HttpClient())
{
    var cts = new CancellationTokenSource()
    var responseTask = client.GetAsync(urlToInvoke, cts.Token);

    cts.Cancel();
}

but this does not

using (var client = new HttpClient())
{
    var cts = new CancellationTokenSource()
    var responseTask = client.GetAsync(urlToInvoke, cts.Token);

    Task.Delay(5000).Wait()
    cts.Cancel();
}
Chris Marisic
  • 32,487
  • 24
  • 164
  • 258
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Great illustration, all my focus was on the TPL and not once had I considered the implications of the HttpClient disposal. – Chris Marisic Aug 21 '14 at 18:15