了解 async / await 和 Task.Run()
Posted
技术标签:
【中文标题】了解 async / await 和 Task.Run()【英文标题】:Understanding async / await and Task.Run() 【发布时间】:2018-04-24 14:36:40 【问题描述】:我以为我理解async
/await
和Task.Run()
非常好,直到我遇到这个问题:
我正在使用 RecyclerView
和 ViewAdapter
编写 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 不够熟悉,不知道发生了哪些事情。我可以说的是,添加async
和await
因为你没有改变代码的运行方式,也不会在UI线程上运行它。以上是关于了解 async / await 和 Task.Run()的主要内容,如果未能解决你的问题,请参考以下文章