【问题标题】:Do you have to put Task.Run in a method to make it async?您是否必须将 Task.Run 放入使其异步的方法中?
【发布时间】:2013-06-11 17:28:10
【问题描述】:

我试图以最简单的形式理解异步等待。为了这个例子,我想创建一个非常简单的方法,将两个数字相加,当然,这根本不需要处理时间,只是在这里制定一个例子。

示例 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

示例 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

如果我等待DoWork1Async(),代码是同步运行还是异步运行?

我是否需要用Task.Run 包装同步代码以使方法可等待和异步,以免阻塞 UI 线程?

我想弄清楚我的方法是 Task 还是返回 Task<T> 是否需要用 Task.Run 包装代码以使其异步。

我敢肯定,这是个愚蠢的问题,但我在网上看到一些示例,其中人们正在等待内部没有任何异步且未包含在 Task.RunStartNew 中的代码。

【问题讨论】:

  • 你的第一个 sn-p 没有给你任何警告吗?

标签: c# .net-4.5 async-await c#-5.0


【解决方案1】:

首先,让我们澄清一些术语:“异步”(async)意味着它可能会在调用线程开始之前将控制权交还给调用线程。在async 方法中,那些“屈服”点是await 表达式。

这与术语“异步”非常不同,因为 MSDN 文档多年来(误用)来表示“在后台线程上执行”。

为了进一步混淆这个问题,async 与“等待”非常不同;有一些 async 方法的返回类型不是可等待的,许多方法返回的可等待类型不是 async

关于他们不是的内容已经足够了;他们是这样的

  • async 关键字允许异步方法(即,它允许await 表达式)。 async 方法可能返回 TaskTask<T> 或(如果必须)void
  • 任何遵循特定模式的类型都可以等待。最常见的等待类型是 TaskTask<T>

因此,如果我们将您的问题重新表述为“我怎样才能以一种可等待的方式在后台线程上运行操作”,答案是使用Task.Run

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(但这种模式是一种糟糕的方法;见下文)。

但是,如果您的问题是“我如何创建一个 async 方法,该方法可以返回给它的调用者而不是阻塞”,答案是声明方法 async 并使用 await 作为它的“让步”分:

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

因此,事物的基本模式是让async 代码依赖于其await 表达式中的“awaitables”。这些“等待对象”可以是其他 async 方法或只是返回等待对象的常规方法。返回Task/Task&lt;T&gt;的常规方法可以使用Task.Run在后台线程上执行代码,或者(更常见的是)它们可以使用TaskCompletionSource&lt;T&gt;或其快捷方式之一(TaskFactory.FromAsyncTask.FromResult 等)。我建议将整个方法包装在 Task.Run 中;同步方法应该有同步签名,是否应该包装在Task.Run中应该由消费者决定:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

我的博客上有async/await intro;最后是一些很好的后续资源。 async 的 MSDN 文档也非常好。

【讨论】:

  • @sgnsajgon:是的。 async 方法必须返回 TaskTask&lt;T&gt;voidTaskTask&lt;T&gt; 可以等待; void 不是。
  • 实际上,async void 方法签名将编译,这只是一个非常糟糕的想法,因为您失去了指向异步任务的指针
  • @TopinFrassi:是的,它们会编译,但 void 不可等待。
  • @ohadinho:不,我在博文中所说的是整个方法只是对Task.Run 的调用(如此答案中的DoWorkAsync)。使用Task.Run调用 UI 上下文中的方法是合适的(如DoVariousThingsFromTheUIThreadAsync)。
  • 是的,完全正确。使用Task.Run invoke 方法是有效的,但如果在方法的所有(或几乎所有)代码周围有一个Task.Run,那么这是一种反模式 - 只需保持该方法同步并将Task.Run 上移一级。
【解决方案2】:

使用 async 装饰方法时要记住的最重要的事情之一是至少有 一个 await 方法内的运算符。在您的示例中,我将使用TaskCompletionSource 将其翻译如下所示。

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async Task DoWork()
{
    int result = await DoWorkAsync();
}

【讨论】:

  • 为什么要使用TaskCompletionSource,而不是仅仅返回Task.Run()方法返回的任务(并改变它的body来返回结果)?
  • 只是一个旁注。具有“async void”签名的方法通常是不好的做法,并且被认为是错误的代码,因为它很容易导致 UI 死锁。主要的例外是异步事件处理程序。
  • 不知道为什么async void 被认为是“不好的做法”,有很多应用程序你会使用它,基本上任何时候你需要做一些你不关心的事情什么时候结束。
【解决方案3】:

当您使用 Task.Run 运行方法时,Task 从线程池中获取一个线程来运行该方法。所以从 UI 线程的角度来看,它是“异步”的,因为它不会阻塞 UI 线程。这对于桌面应用程序来说很好,因为您通常不需要很多线程来处理用户交互。

但是,对于 Web 应用程序,每个请求都由一个线程池线程提供服务,因此可以通过保存这些线程来增加活动请求的数量。频繁使用线程池线程来模拟异步操作对于 Web 应用程序是不可扩展的。

真正的异步不一定涉及使用线程进行 I/O 操作,例如文件/数据库访问等。您可以阅读本文以了解为什么 I/O 操作不需要线程。 http://blog.stephencleary.com/2013/11/there-is-no-thread.html

在您的简单示例中,这是一个纯 CPU 密集型计算,因此使用 Task.Run 就可以了。

【讨论】:

  • 所以如果我必须在 web api 控制器中使用同步外部 api,我不应该在 Task.Run() 中包装同步调用吗?正如您所说,这样做将使初始请求线程保持畅通,但它正在使用另一个池线程来调用外部 api。事实上,我认为这仍然是一个好主意,因为这样做理论上可以使用两个池线程来处理许多请求,例如一个线程可以处理许多传入的请求,而另一个线程可以为所有这些请求调用外部 api?
  • 我同意。我并不是说您绝对不应该将所有同步调用都包含在 Task.Run() 中。我只是指出潜在的问题。
  • @stt106 I should NOT wrap the synchronous call in Task.Run() 这是正确的。如果你这样做,你只是在切换线程。即您正在解除对初始请求线程的阻塞,但您正在从线程池中获取另一个线程,该线程可能用于处理另一个请求。唯一的结果是当调用以绝对零增益完成时的上下文切换开销
猜你喜欢
  • 1970-01-01
  • 2018-05-15
  • 2020-02-15
  • 1970-01-01
  • 2019-10-09
  • 1970-01-01
  • 1970-01-01
  • 2021-04-18
  • 2012-12-06
相关资源
最近更新 更多