Understanding async / await and Task.Run()
Solution 1
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");});
}
Solution 2
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
Solution 3
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.
Related videos on Youtube
Tobias von Falkenhayn
Updated on June 17, 2022Comments
-
Tobias von Falkenhayn almost 2 years
I thought I understood
async
/await
andTask.Run()
quite well until I came upon this issue:I'm programming a Xamarin.Android app using a
RecyclerView
with aViewAdapter
. In my OnBindViewHolder Method, I tried to async load some imagespublic 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 onimageView.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.
-
johnny 5 about 6 yearsyour code does have a crash you're just ignoring it. Async-> Void is Fire and forget which is bad you should be returning a task
-
Tobias von Falkenhayn about 6 years@johnny5 no it's not. its called on an UI event, where async void is totally "legal".
-
johnny 5 about 6 yearsOops, didn't realize you were calling an event, I was expecting EventArgs, void is legal, but it's still fire and forget, nothing will catch the exceptions
-
Funk about 6 yearsjohnny 5 is right, wrap
imageView.SetImageBitmap(bitmap);
in a try/catch block and you'll find the sameex.Message
. This post might be of interest (note the quote). -
noseratio about 6 yearsAdd
Debug.WriteLine($"thread: {System.Threading.Thread.Currentthread.Managedthreadid}")
before and after eachawait
. What's the output?
-
-
Tobias von Falkenhayn about 6 yearsSorry that was a mistake, the OnBindViewHolder is a Non-Synchronous method. It must be void cause its an Event. Doesn't Task.Run() marshall the delegate to a thread within the threadpool?
-
Servy about 6 yearsUsing Task.Run doesn't make an async operation synchronous. It just runs a delegate in a thread pool thread. Nothing here explains why the use of a UI element doesn't fail when using
Task.Run
. Additionally the event handler can't return a task, because it's an event handler. -
Matti Price about 6 years@Servy is right, I misspoke, I need to edit that in a bit, typing to quick before a meeting
-
Servy about 6 yearsEverything after the first sentence is wrong. Adding that await in there changes literally nothing about how the code behaves, and the only code that the OP has shown that ever runs on the UI thread is the code that schedules an operation to run in a thread pool thread..
-
Matti Price about 6 yearsAnd yet, the OP clearly stated that it was working with Task.Run so obviously that code is in fact running on the UI thread. Feel free to provide another answer as to why though @Servy
-
Servy about 6 yearsNot necessarily. It's possible that the operation doesn't always successfully fail when it's not run on the UI thread, or the type they're using could, under certain circumstances, marshal to the UI thread when performing that operation, or other possibilities. I'm not familiar enough with xamarin to know which of those things is going on. What I can say is that adding the
async
andawait
as you have changes nothing about how the code runs, nor does it run it on the UI thread. -
Matti Price about 6 years
-
Matti Price about 6 years@Servy adding async/await wasn't intended to run on the UI thread which is why I said I would expect it to actually throw the error the OP is expecting. What is likely happening is that since the async call inside the Task.Run is not being awaited, the ConfigureAwait is not being respected. Because no other work is being done in the the main UI thread, it is considered free, and picks up the Task.Run queued work to execute. The result is that the code is executed ont he main thread. Adding async/await as I showed I believe would change that behavior and throw the error that the OP expected
-
Servy about 6 years
Task.Run
specifically queues work in the thread pool. It doesn't just randomly choose to run the code in the UI thread, even if the UI thread is free. You're correct that theConfigureAwait
in the Task.Run call doesn't do anything and can be removed. Addingasync
andawait
to the lambda just means that the code that runs afterLoadImage
(i.e. nothing at all) will actually care about how the await is configured, so you're just stacking more superfluous code on top of existing superfluous code. -
Matti Price about 6 yearsI believe adding the await inside the lambda will trigger the exception that the OP expects (about it not running on the UI thread) and explain the behavior they are seeing. If you have another explanation for what the OP is seeing, I'm all ears.
-
Tobias von Falkenhayn about 6 yearsSo the real question fot me is: Why does imageView.SetBitmapInage NOT crash when called inside the Task.Run? Since it marshalls the code execution to a thread in the threadpool, and the imageview sits on the UI thread, it should crash.
-
Matti Price about 6 yearsThat's what I was getting at in my answer. It is being assigned to run on the thread pool, but since the UI thread isn't busy, the UI thread is the one picking up the work. That's what I'm pretty sure is happening.. I think it is more an oddity than correct code. Try that async/await Task.Run I posted though. I'm curious if that will error or not.
-
Tobias von Falkenhayn about 6 yearsThanks for your answer, but like you said when Task.Run forces a function to run on the Thread Pool - not the UI Thread - shouldn't be calling any UI-Element on this function result in an error? Like i did: Task.Run(update Image here) <= why does this not fail?
-
Dennis Schröer about 6 yearsIt may as well crash, but it seems like Xamarin is shifting some work to the UI thread. But it's important to understand that 1. it could crash the way you implemented it and 2. you are running code in a thread pool which should be called on the UI thread, which means performance losses for you. In the first link I provided in my answer, you can read more about the reasons why you lose performance by using
await Task.Run()
. -
Tobias von Falkenhayn about 6 yearsSo maybe the UI Thread is also part of the "Thread Pool" in Xamarin? I know that it's bad code but i was just wondering why it didn't crash when it should.
-
Dennis Schröer about 6 yearsMaybe, I don't know how exactly it's working. It's more important to understand that it could as well crash and that it's much better performance if you run it directly on the UI Thread.
-
SushiHangover about 6 years@DennisSchröer
Task.Run() blocks a whole thread pool.
It blocks a thread in the ThreadPool, not the entire pool of threads -
Leonid Vasilev about 6 yearsWhat makes you think that Xamarin delegates some action subroutine execution to a UI thread?
-
Matti Price about 6 years@LeonidVasilyev OP said that the code "worked" which would indicate it updated the UI as expected, which means it didn't crash. Like I said in my (downvoted) answer is my best guess as to why it didn't crash. It probably isn't guaranteed not to crash though, and definitely isn't a "good" way to do it. But it does explain the behavior of it not bombing out.