简体   繁体   中英

Understanding async / await and Task.Run()

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 .
  • 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

It's as simple as you not awaiting the Task.Run, so the exception gets eaten and not returned to the call site of Task.Run.

Add "await" in front of the Task.Run, and you'll get the exception.

This will not crash your application:

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

This however will crash your application:

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

Probably UI access still throws UIKitThreadAccessException . You do not observe it because you do not use await keyword or Task.Wait() on a marker that Task.Run() returns. See Catch an exception thrown by an async method discussion on StackOverflow, MSDN documentation on the topic is a bit dated.

You can attach continuation to the marker that Task.Run() returns and inspect exceptions thrown inside an action passed:

Task marker = Task.Run(() => ...);
marker.ContinueWith(m =>
{
    if (!m.IsFaulted)
        return;

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

In general, UI access from non UI thread may make an application unstable or crash it, but it isn't guaranteed.

For more information check How to handle Task.Run Exception , Android - Issue with async tasks discussions on StackOverflow, The meaning of TaskStatus article by Stephen Toub and Working with the UI Thread article on Microsoft Docs.

Task.Run is queuing LoadImage to execute the async process on the thread pool with ConfigureAwait(false) . The task that LoadImage is returning is NOT awaited though and I believe that is the important part here.

So the results of Task.Run is that it immediately returns a Task<Task> , but the outer task does not have ConfigureAwait(false) set, so the whole thing resolves on the main thread instead.

If you change your code to

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

I'm expecting you to hit the error that the thread isn't running on the UI thread.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM