了解 async / await 和 Task.Run()

Posted

技术标签:

【中文标题】了解 async / await 和 Task.Run()【英文标题】:Understanding async / await and Task.Run() 【发布时间】:2018-04-24 14:36:40 【问题描述】:

我以为我理解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;

那段代码工作。但为什么?

据我所知,configure await 设置为 false 后,代码不会在SynchronizationContext(即 UI 线程)中运行。

如果我将OnBindViewHolder 方法设为异步并使用await 而不是Task.Run,​​则代码在

上崩溃
imageView.SetImageBitmap(bitmap);

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

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

更新:回答

由于未等待 Task.Run,​​因此未显示引发的异常。如果我等待 Task.Run,​​就会出现我预期的错误。在下面的答案中可以找到进一步的解释。

【问题讨论】:

您的代码确实发生了崩溃,您只是忽略了它。 Async-> Void is Fire 忘记了你应该返回一个任务是什么 @johnny5 不,不是。它在 UI 事件中调用,其中 async void 是完全“合法的”。 哎呀,没有意识到你在调用一个事件,我期待 EventArgs,void 是合法的,但它仍然是触发并忘记,没有什么会捕获异常 johnny 5 是对的,将 imageView.SetImageBitmap(bitmap); 包装在 try/catch 块中,您会发现相同的 ex.Message。这个post 可能很有趣(注意引用)。 在每个await 前后添加Debug.WriteLine($"thread: System.Threading.Thread.Currentthread.Managedthreadid")。输出是什么? 【参考方案1】:

就像您不等待 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"););

【讨论】:

【参考方案2】:

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

Task.Run() 应用于CPU 绑定方法。 UI-Thread 应该用于UI相关的方法

通过将代码移动到Task.Run(),可以避免 UI 线程被阻塞。这可能会解决您的问题,但这不是最佳实践,因为它不利于您的表现。 Task.Run() 阻塞线程池中的一个线程。

您应该做的是在 UI 线程上调用与 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

【讨论】:

感谢您的回答,但正如您所说,当 Task.Run 强制函数在线程池(而不是 UI 线程)上运行时,不应在此函数上调用任何 UI 元素导致一个错误?就像我做的那样:Task.Run(update Image here) 它也可能会崩溃,但似乎 Xamarin 正在将一些工作转移到 UI 线程。但重要的是要了解 1. 它可能会崩溃 您实现它的方式以及 2. 您正在线程池中运行代码,该代码应该在 UI 线程上调用,这意味着您的性能损失。在我在回答中提供的第一个链接中,您可以通过使用await Task.Run() 阅读更多关于性能下降的原因。 那么也许 UI 线程也是 Xamarin 中“线程池”的一部分?我知道这是糟糕的代码,但我只是想知道为什么它没有崩溃。 也许,我不知道它到底是如何工作的。更重要的是要了解它也可能会崩溃并且如果您直接在 UI 线程上运行它会获得更好的性能。 @DennisSchröer Task.Run() blocks a whole thread pool. 它阻塞了线程池中的一个线程,而不是整个线程池【参考方案3】:

可能 UI 访问仍然会抛出 UIKitThreadAccessException。您没有观察到它,因为您没有在 Task.Run() 返回的标记上使用 await 关键字或 Task.Wait()。请参阅 *** 上的 Catch an exception thrown by an async method 讨论,有关该主题的 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 访问可能会使应用程序不稳定或崩溃,但不能保证。

有关详细信息,请查看 *** 上的 How to handle Task.Run Exception、Android - Issue with async tasks 讨论、Stephen Toub 的 The meaning of TaskStatus 文章和 Microsoft Docs 上的 Working with the UI Thread 文章。

【讨论】:

【参考方案4】:

Task.Run 正在排队 LoadImage 以使用 ConfigureAwait(false) 在线程池上执行异步进程。 LoadImage 返回的任务并没有等待,我相信这是这里的重要部分。

所以Task.Run的结果是它立即返回一个Task<Task>,但是外部任务没有设置ConfigureAwait(false),所以整个事情转而在主线程上解决。

如果您将代码更改为

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

我希望您遇到线程未在 UI 线程上运行的错误。

【讨论】:

抱歉打错了,OnBindViewHolder 是一个非同步方法。它必须是无效的,因为它是一个事件。 Task.Run() 不会将委托编组到线程池中的线程吗? 使用 Task.Run 不会使异步操作同步。它只是在线程池线程中运行一个委托。这里没有解释为什么在使用Task.Run 时使用 UI 元素不会失败。此外,事件处理程序不能返回任务,因为它是一个事件处理程序。 @Servy 是对的,我说错了,我需要稍微编辑一下,在会议前快速输入 第一句后面的都是错的。在其中添加等待实际上不会改变代码的行为方式,并且 OP 显示的唯一在 UI 线程上运行的代码是调度操作以在线程池线程中运行的代码.. 不一定。当操作不在 UI 线程上运行时,操作并不总是成功失败,或者在某些情况下,它们使用的类型可能会在执行该操作或其他可能性时编组到 UI 线程。我对 xamarin 不够熟悉,不知道发生了哪些事情。我可以说的是,添加asyncawait 因为你没有改变代码的运行方式,也不会在UI线程上运行它。

以上是关于了解 async / await 和 Task.Run()的主要内容,如果未能解决你的问题,请参考以下文章

async和await浅析

异步操作要了解的ES7的async/await

async/await使用深入详解

promise和async/await

Kotlin Coroutine,Android Async Task 和 Async await 的区别

promise和async/await