理解async/await和Task.Run()

27

我原本以为我对async/awaitTask.Run()非常了解,直到我遇到了这个问题:

我正在使用RecyclerViewViewAdapter编写一个Xamarin.Android应用程序。在我的OnBindViewHolder方法中,我尝试异步加载一些图片。

public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
    // Some logic here

    Task.Run(() => LoadImage(postInfo, holder, imageView).ConfigureAwait(false)); 
}

然后,在我的LoadImage函数中,我做了像这样的事情:

private async Task LoadImage(PostInfo postInfo, RecyclerView.ViewHolder holder, ImageView imageView)
{                
    var image = await loadImageAsync((Guid)postInfo.User.AvatarID, EImageSize.Small).ConfigureAwait(false);
    var byteArray = await image.ReadAsByteArrayAsync().ConfigureAwait(false);

    if(byteArray.Length == 0)
    {
        return;
    }

    var bitmap = await GetBitmapAsync(byteArray).ConfigureAwait(false);

    imageView.SetImageBitmap(bitmap);
    postInfo.User.AvatarImage = bitmap;
}

那些代码运行了。但为什么呢?

在将“配置等待”设置为false后,我学到的是代码不会在SynchronizationContext(即UI线程)中运行。

如果我将OnBindViewHolder方法设为async并使用await代替Task.Run,则代码会崩溃。

imageView.SetImageBitmap(bitmap);

说它不在UI线程中,这对我来说完全有意义。

那么为什么async/await代码崩溃而Task.Run()没有呢?

更新:答案

因为Task.Run没有被等待,所以抛出的异常没有显示。如果我等待Task.Run,就会出现我预期的错误。更多解释可以在下面的答案中找到。


2
你的代码确实有崩溃,但你却忽略了它。Async->Void是“Fire and forget”,这是不好的,你应该返回一个任务。 - johnny 5
1
@johnny5 不是这样的。它被称为 UI 事件,其中 async void 是完全“合法”的。 - Tobias von Falkenhayn
哎呀,我没意识到你在调用一个事件,我本来期望的是 EventArgs,void 是合法的,但它仍然是“fire and forget”的,没有任何东西会捕获异常。 - johnny 5
1
Johnny 5是正确的,将imageView.SetImageBitmap(bitmap);放入try/catch块中,您将找到相同的ex.Message。这篇文章可能会引起兴趣(注意引用)。 - Funk
在每个 await 前后添加 Debug.WriteLine($"thread: {System.Threading.Thread.Currentthread.Managedthreadid}")。输出是什么? - noseratio - open to work
4个回答

14

只要您不等待Task.Run,异常就会被吞噬而不会返回到Task.Run的调用站点。

在Task.Run前加上"await",您将得到异常。

这不会导致您的应用程序崩溃:

private void button1_Click(object sender, EventArgs e)
{
    Task.Run(() => { throw new Exception("Hello");});
}

然而,这将会导致您的应用程序崩溃:

private async void button1_Click(object sender, EventArgs e)
{
   await Task.Run(() => { throw new Exception("Hello");});
}

13

Task.Run() 和 UI 线程应该用于不同的目的:

  • Task.Run() 应该用于需要大量 CPU 运算的方法
  • UI 线程应该用于与界面相关的方法

将代码移动到 Task.Run() 可以避免阻塞 UI 线程,这可能会解决您的问题,但不是最佳实践,因为它会对性能产生负面影响。 Task.Run() 会阻塞线程池中的一个线程。

您应该在 UI 线程上调用与界面相关的方法。在 Xamarin 中,您可以使用 Device.BeginInvokeOnMainThread() 在 UI 线程上运行程序:

// async is only needed if you need to run asynchronous code on the UI thread
Device.BeginInvokeOnMainThread(async () =>
{
    await LoadImage(postInfo, holder, imageView).ConfigureAwait(false)
});
即使您没有在UI线程上显式调用它,它仍然有效的原因可能是因为Xamarin会检测到它应该在UI线程上运行,并将此工作转移到UI线程上。以下是Stephen Cleary撰写的一些有用文章,这些文章帮助我编写了这个答案,也将帮助您进一步理解异步代码: https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

1
它可能会崩溃,但似乎Xamarin正在将一些工作转移到UI线程上。但重要的是要理解:1.它可能会以你实现的方式崩溃;2.你正在线程池中运行代码,应该在UI线程上调用,这意味着你会损失性能。在我回答中提供的第一个链接中,您可以阅读有关使用await Task.Run()会导致性能损失的原因的更多信息。 - Dennis Schröer
也许我不知道它的确切工作方式。更重要的是要理解它很可能会崩溃,并且如果您直接在 UI 线程上运行它,性能会更好。 - Dennis Schröer
1
@DennisSchröer Task.Run() 阻塞整个线程池。它只会阻塞线程池中的一个线程,而不是整个线程池。 - SushiHangover
你为什么认为Xamarin将某些操作子例程的执行委托给UI线程? - Leonid Vasilev
@LeonidVasilyev OP说代码“工作了”,这意味着它按预期更新了UI,也就是说它没有崩溃。正如我在我的(被踩)答案中所说的那样,这是我为什么认为它不会崩溃的最佳猜测。虽然它可能不能保证不崩溃,而且绝对不是一个“好”的方法来做到这一点。但它确实解释了它不会崩溃的行为。 - Matti Price
显示剩余2条评论

3
可能仍然会抛出UI访问异常UIKitThreadAccessException,但如果您没有在Task.Run()返回的标记上使用await关键字或Task.Wait(),则不会观察到它。请参见StackOverflow上的捕获异步方法抛出的异常讨论,MSDN文档有点过时。
您可以将继续附加到Task.Run()返回的标记,并检查传递的操作中抛出的异常:
Task marker = Task.Run(() => ...);
marker.ContinueWith(m =>
{
    if (!m.IsFaulted)
        return;

    // Marker Faulted status indicates unhandled exceptions. Observe them.
    AggregateException e = m.Exception;
});

一般来说,非 UI 线程访问 UI 可能会导致应用程序不稳定或崩溃,但并不是一定会发生。
有关更多信息,请参阅 StackOverflow 上的 如何处理 Task.Run 异常Android - 异步任务问题 讨论,Stephen Toub 的 TaskStatus 的含义 文章以及 Microsoft Docs 上的 与 UI 线程一起工作 文章。

-2

Task.RunLoadImage 排入线程池执行异步进程,并使用 ConfigureAwait(false)。但是,LoadImage 返回的任务未被等待,我认为这很重要。

因此,Task.Run 的结果是立即返回一个 Task<Task>,但外部任务没有设置 ConfigureAwait(false),因此整个过程会在主线程上解决。

如果您更改您的代码为

Task.Run(async () => await LoadImage(postInfo, holder, imageView).ConfigureAwait(false)); 

我希望你会遇到线程没有在 UI 线程上运行的错误。


抱歉,那是一个错误,OnBindViewHolder是一个非同步方法。它必须是void类型,因为它是一个事件。Task.Run()会将委托封送到线程池中的线程吗? - Tobias von Falkenhayn
4
使用Task.Run并不能使异步操作变为同步操作,它只是在线程池线程中运行一个委托。这里并没有解释为什么使用Task.Run时使用UI元素不会失败。此外,事件处理程序无法返回任务,因为它是一个事件处理程序。 - Servy
@Servy 是正确的,我说错了,我需要稍微编辑一下,在会议之前打字太快了。 - Matti Price
1
第一句话之后的所有内容都是错误的。 在那里添加await对代码行为几乎没有任何影响,而且OP显示出来的唯一在UI线程上运行的代码是计划在线程池线程中运行操作的代码。 - Servy
1
不一定。如果在非 UI 线程运行操作时,有可能操作并不总是会成功失败;或者他们所使用的类型,在执行该操作时会在某些情况下转移到 UI 线程,还有其他可能性。我对 Xamarin 的了解程度不足,无法确定到底发生了哪些事情。但我可以说的是,就代码运行方式而言,你所添加的 asyncawait 并没有改变任何内容,也没有将其放到 UI 线程上运行。 - Servy
显示剩余7条评论

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