I thought I understood async
/await
and Task.Run()
quite well until I came upon this issue:
I'm programming a Xamarin.Android app using a RecyclerView
with a ViewAdapter
. In my OnBindViewHolder Method, I tried to async load some images
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
// Some logic here
Task.Run(() => LoadImage(postInfo, holder, imageView).ConfigureAwait(false));
}
Then, in my LoadImage function I did something like:
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;
}
That pieces of code worked. But why?
What I've learned, after configure await is set to false, the code doesn't run in the SynchronizationContext
(which is the UI thread).
If I make the OnBindViewHolder
method async and use await instead of Task.Run, the code crashes on
imageView.SetImageBitmap(bitmap);
Saying that it's not in the UI thread, which makes totally sense to me.
So why does the async
/await
code crash while the Task.Run() doesn't?
Update: Answer
Since the Task.Run was not awaited, the thrown exception was not shown. If I awaitet the Task.Run, there was the error i expected. Further explanations are found in the answers below.
Task.Run()
and the UI thread should be used for a different purpose:
Task.Run()
should be used for CPU-bound methods.By moving your code into Task.Run()
, you avoid the UI thread from being blocked. This may solve your issue, but it's not best practice because it's bad for your performance. Task.Run()
blocks a thread in the thread pool.
What you should do instead is to call your UI related method on the UI thread. In Xamarin, you can run stuff on the UI thread by using Device.BeginInvokeOnMainThread()
:
// 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)
});
The reason why it's working even if you don't explicitly call it on the UI thread is probably because Xamarin somehow detects that it's something that should run on the UI thread and shifts this work to the UI thread.
Here are some useful articles by Stephen Cleary which helped me to write this answer and which will help you to further understand asynchronous code:
https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html