Return 将旧的异步模式包装到 TaskCompletionSource 时使用 await?

Return with await when wrapping old async pattern into TaskCompletionSource?

我正在研究 C# Asnc-await 模式,目前正在阅读 S. Cleary 的 C# Cookbook 中的并发性

他讨论了将带有 TaskCompletionSource (TCS) 的旧非 TAP 异步模式包装到 TAP 结构中。 我不明白的是,为什么他只是 returns TCS 对象的任务 属性 而不是等待它 TCS.Task ?

示例代码如下:

旧的包装方法是 DownloadString(...):

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

将其包装到 TAP 结构中:

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;
}

现在为什么不那样做呢:

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;
}

两者在功能上有区别吗?第二个不是更自然吗?

您的版本更加复杂——您不是只返回任务,而是使方法异步,并等待您可能刚刚返回的任务。

有一个细微的实际差异(除了 await 比 运行 慢的版本)。

在第一个示例中,如果 DownloadString 抛出异常(而不是调用您通过 exception 设置的委托),那么该异常将通过您对 [=15= 的调用冒泡].

第二种,异常被打包到DownloadStringAsync返回的Task中。

所以,假设 DownloadString 抛出这个异常(并且没有其他异常发生):

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
}

您可能不关心区别 - 如果您只写:

await httpService.DownloadStringAsync(...);

您不会注意到差异。

同样,这只有在 DownloadString 方法本身抛出时才会发生。如果它改为调用你给它的委托并将 exception 设置为一个值,那么你的两种情况之间没有明显的区别。

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

您不必将自己的方法标记为 async 即可获得 "Task not awaited" 警告。以下代码对 TU 的调用生成相同的警告:

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;
}

每当你发现自己的方法只包含一个 await 并且这是它做的最后一件事(可能 returning 等待值除外),你应该问问自己它是什么值添加。除了异常处理方面的一些差异外,它只是在组合中添加了一个额外的 Task

await 通常是一种指示 "I've got no useful work to do right now, but will have when this other Task is finished" 的方式,这当然不是真的(您以后没有其他工作要做)。因此,请跳过 await,只 return 您所期待的。

朋友们,感谢您的宝贵意见。

与此同时,我阅读了此处引用的资源并进一步调查了此事:受 https://blog.stephencleary.com/2016/12/eliding-async-await.html 的影响我得出的结论是,即使在同步中,默认情况下也最好包含 async-await异步函数链的方法,并且仅在情况明确表明该方法的行为可能与边缘场景中的异步方法的预期行为不同时才省略异步等待。
如:同步方法短小精悍,内部没有任何可能抛出异常的操作。如果同步方法抛出异常,则调用者将在调用行而不是等待任务的行中获得异常。这显然是对预期行为的改变。

例如将调用参数原封不动地移交给下一层是imo允许省略async-await的情况。

阅读我的回答为什么返回任务不是一个好主意:What is the purpose of "return await" in C#?

基本上,如果不使用 await,就会破坏调用堆栈。