Understanding async / await and Task.Run()

Tobias von Falkenhayn picture Tobias von Falkenhayn · Apr 24, 2018 · Viewed 8.1k times · Source

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.

Answer

Dennis Schröer picture Dennis Schröer · Apr 26, 2018

Task.Run() and the UI thread should be used for a different purpose:

  • Task.Run() should be used for CPU-bound methods.
  • UI-Thread should be used for UI related 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