1
votes

I am studying C# Asnc-await pattern and currently reading Concurrency in C# Cookbook from S. Cleary

He discusses wrapping old non TAP async patterns with TaskCompletionSource (TCS) into TAP constructs. What I dont get is, why he just returns the Task property of the TCS object instead of awaiting it TCS.Task ?

Here is the example code:

Old method to wrap is DownloadString(...):

public interface IMyAsyncHttpService
{
    void DownloadString(Uri address, Action<string, Exception> callback);
}

Wrapping it into TAP construct:

public static Task<string> DownloadStringAsync(
    this IMyAsyncHttpService httpService, Uri address)
{
    var tcs = new TaskCompletionSource<string>();
    httpService.DownloadString(address, (result, exception) =>
    {
        if (exception != null)
            tcs.TrySetException(exception);
        else
            tcs.TrySetResult(result);
    });
    return tcs.Task;
}

Now why not just do it that way:

public static async Task<string> DownloadStringAsync(
    this IMyAsyncHttpService httpService, Uri address)
{
    var tcs = new TaskCompletionSource<string>();
    httpService.DownloadString(address, (result, exception) =>
    {
        if (exception != null)
            tcs.TrySetException(exception);
        else
            tcs.TrySetResult(result);
    });
    return await tcs.Task;
}

Is there a functional difference between the two? Is the second one not more natural?

5
Oh, how difficult it may be to read an unindented code. This is an additional Task which doesn't bring any improvement. You can just return a Task, but instead you create a state machine, which will just wait for this task and then return the same result. - Yeldar Kurmangaliyev
This article should actually answer your question: stackoverflow.com/questions/19098143/…. - Yeldar Kurmangaliyev
As others have said, it's more complex. It might be useful for you to describe why you think the "just do it that way" version is meant to be an improvement. Remember, await is a way of dealing with existing running Tasks. It doesn't start any new work itself. - Damien_The_Unbeliever
My thought process was, that since the original method was converted to a TAP construct, the user of the code will surely await the converted method (since it is TAP and awaiting running tasks at some point is how it is supposed to be used). By marking it async, the compiler will generate warnings, that it should be considered to await this method. So imo. this well have a more TAP look and feel from users perspective - J4ni
You're still returning a Task. The caller of this method always (*1) has to await that Task if they need to know when the job is done. (*1 - or other moral equivalents) - Damien_The_Unbeliever

5 Answers

1
votes

By marking it async, the compiler will generate warnings, that it should be considered to await this method

You don't have to mark your own method as async in order to get the "Task not awaited" warning. The following code generates the same warning for the calls to both T and U:

static async Task Main(string[] args)
{
    Console.WriteLine("Done");
    T();
    U();
    Console.WriteLine("Hello");
}

public static Task T()
{
    return Task.CompletedTask;
}

public static async Task U()
{
    await Task.Yield();
    return;
}

Whenever you find yourself with a method only containing a single await and that being the last thing it does (except possibly returning the awaited value), you should ask yourself what value it's adding. Aside from some differences in exception handing, it's just adding an extra Task into the mix.

await is generally a way of indicating "I've got no useful work to do right now, but will have when this other Task is finished" which of course isn't true (you've got no other work to do later). So skip the await and just return what you would have awaited instead.

0
votes

Your version is strictly more complex -- instead of just returning the task, you make the method async, and await the task you could just be returning.

0
votes

There is one subtle practical difference (other than the version with await being slower to run).

In the first example, if DownloadString throws an exception (rather than calling the delegate you pass it with exception set), then that exception will bubble through your call to DownloadStringAsync.

In the second, the exception is packaged into the Task returned from DownloadStringAsync.

So, assuming that DownloadString throws this exception (and no other exceptions occur):

Task<string> task;
try
{
    task = httpService.DownloadStringAsync(...);
}
catch (Exception e)
{
    // Catches the exception ONLY in your first non-async example
}

try 
{
    await task;
}
catch (Exception e)
{
    // Catches the exception ONLY in your second async example
}

You probably don't care about the distinction - if you just write:

await httpService.DownloadStringAsync(...);

you won't notice the difference.

Again, this only happens if the DownloadString method itself throws. If it instead calls the delegate you give it with exception set to a value, then there is no observable difference between your two cases.

0
votes

Folks, thanks for the helpful comments.

In the meantime I have read the sources referenced here and also investigated the matter further: Influenced by https://blog.stephencleary.com/2016/12/eliding-async-await.html I have come to the conclusion, that its best practice to include async-await by default even in synchronous methods of the async function chain and only omit async await when the circumstances clearly indicate that the method will not potentially behave differently as expected from an async method en edge scenarios.
Such as: the synchronous method is short, simple and has no operations inside which could throw an exception. If the synchronous method throws an exception the caller will get an exception in the calling line instead of the line where the Task is awaited. This is clearly a change from expected behavior.

For example handing over call parameters to the next layer unchanged is a situation which imo permits omitting async-await.

0
votes

Read my answer why returning the task is not a good idea: What is the purpose of "return await" in C#?

Basically you break your call stack if you don't use await.