如何正确创建一个可等待的方法

4
我是一名有用的助手,可以为您翻译文本。

我正在编写一些可等待的方法,并在互联网上找到了许多可以这样做的方式。所以我来这里了解每种方式的实际情况,以及是否必须淘汰某些方式。

据我所知,有两种可等待的方法:

调用其他可等待方法的方法:

public async Task<Foo> GetFooAsync()
{
    var foo = await TrulyGetFooAsync();

    // Do stuff with foo

    return foo;
}

我没有发现其他做法,我认为这是正确的方法。如果我错了,请告诉我!

只调用不可等待方法的函数:

接下来我会讲到一些问题。

例如,我看到了这个:

例子1

public async Task<Foo> GetFooAsync()
{
    return await Task.Run(() => TrulyGetFoo());
}

据我所知,async/await关键字是无用的,可以避免使用,示例如下:

示例2

public Task<Foo> GetFooAsync()
{
    return Task.Run(() => TrulyGetFoo());
}

这是我一直在做的最后一个例子。关于这个问题,下面两种方式有什么不同:

Task.Run(() => TrulyGetFoo());

并且

Task.Run((Foo)TrulyGetFoo); // I don't know if the cast is required at any time but in my code, it was

我最近发现了这种方法:

例子三

public Task<Foo> GetFooAsync()
{
    TaskCompletionSource<Foo> tcs = new TaskCompletionSource<Foo>();
    tcs.SetResult(TrulyGetFoo());
    return tcs.Task;
}

如果我理解正确,可等待方法并不总是在另一个线程上运行?我的猜测是第三个示例提供了这种机制(但是如何实现的?我只看到第三个示例中的同步代码),而示例1和2将始终在工作线程上运行?
可能还有其他编写可等待方法的方式,请告诉我。

另一种方法:public Task<Foo> GetFooAsync() { return TrulyGetFooAsync(); } - Sir Rufo
当然,我太简化了这个例子,你的评论是正确的。我会编辑我的问题以澄清! - fharreau
1
"我只在第三个例子中看到同步代码 - 它完全是同步的。任务将返回已完成状态。在这种情况下,您可以使用return Task.FromResult(TrulyGetFoo())来执行相同的操作。" - Evk
3个回答

3
例如,我看到在同步代码周围有 [Task.Run的包装器]

这是一种不好的做法。我有一篇博客文章解释了 为什么Task.Run不应该被用作方法实现

就我所知,关键字async/await是无用的,可以避免使用

是的,但是我不建议在更复杂的方法中省略 async/await

我最近发现 [TaskCompletionSource<T>]

如评论中所指出的那样,您的代码示例仍然是同步的。

为使其异步化,您的代码应该启动某些操作并返回TaskCompletionSource<T>.Task。然后稍后,每当该操作完成时,您的完成处理程序应调用TaskCompletionSource<T>.SetResult(或类似方法)。例如,请参见TAP wrappers for EAPTAP wrappers for WaitHandlesTaskFactory.FromAsync也是TaskCompletionSource<T>的包装器,并用于TAP wrappers for APM

我正在阅读您在其他答案中提供的Rufo先生的“任务之旅”文章。加上您答案中的所有链接,我今天有一堆有趣的阅读材料。 - fharreau

3
简而言之:任何返回Task的内容都可以在async代码块中被await
public async Task MyAsyncMethod()
{
    // do some stuff
    await TrulyAsyncFoo();
    // do some other stuff
    return;
}

如果等待异步调用是该方法唯一要做的事情,那么您可以简单地返回任务本身,这将在“上游”被等待:

public Task MyAsyncMethod()
{
    return TrulyAsyncFoo();
}

只要在异步方法中调用同步(非异步)代码,就像调用普通代码一样,没有什么特别的要求:
public async Task MyAsyncMethod()
{
    MySyncMethod();
    await TrulyAsyncFoo();
    MyOtherSyncMethod();
}

执行 Task.Run(() => Foo()) 几乎总是一种代码异味,表明您没有正确使用异步/等待。编写异步代码与编写多线程代码不同。异步只是一种向编译器说明需要等待某些网络或IO绑定任务完成的好方法。
总之:
  • 等待让您可以并排编写异步和同步代码
  • 仅应使用 Async 等待网络或 IO 绑定任务,而不是计算绑定任务
  • 异步方法应始终返回 TaskTask<T>
  • 避免使用 async void
  • 除非知道自己在做什么,否则请避免在 ASP.NET 和其他线程应用程序中使用 task.Wait()task.Result 阻塞

4
一个关于async/await/task的好资源是Stephen Cleary的博客,链接为https://blog.stephencleary.com/2014/04/a-tour-of-task-part-0-overview.html。 - Sir Rufo
如果等待异步调用是该方法唯一要做的事情,你可以简单地返回任务本身。如我在评论中对Rufo先生所说,我已经过于简化了示例。我刚刚编辑了我的问题,以获得更好的示例。通常不需要Task.Run(() => Foo())或其他复杂的语法。这几乎总是一个代码异味,说明您没有正确使用async/await。正如您所说“通常”。那么如果我仍然需要呢? - fharreau
从Sir Rufo的链接中:曾经使用过Task的开发人员倾向于在异步世界中以同样的方式使用它(这是错误的)。我想我不止一次陷入了这个陷阱。这两个链接真的非常有趣。谢谢! - fharreau
@fharreau,我稍微澄清了一下我的回答。调用同步方法不应该需要Task.Run - 你具体想做什么? - Nate Barbettini
我正在以两种不同的方式实现一个接口,其中包含返回Task的方法。一种实现方式是使用可等待的方法,所以我没有问题。另一种实现方式只使用不可等待的方法,那么我该如何编写这个实现呢?只需使用Task.FromResult()吗?我正在开发的应用程序是一个WPF应用程序。我不希望在调用期间冻结我的UI。 - fharreau
@fharreau 是的,您可以使用 Task.FromResult 从同步方法返回一个任务。然而,在 WPF 应用程序中处理线程是一个稍微不同的话题,有其自己的“坑点”,因此您可能需要发布另一个问题,并提供您在 WPF 中尝试做什么的详细信息。 - Nate Barbettini

-3

我正在使用它来处理异步事件,以操作我的数据库

private async void btnSave_Click(object sender, EventArgs e)
    {

             success = await someClass.someMethod(some args);

    }

而 someMethod 是一个类似于这样的任务:

public static async Task<someObject> someMethod(args)
    {
       //do something here
    }

这如何回答上面的问题? - Sir Rufo
1
除非绝对必要,否则应避免使用async void - Nate Barbettini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接