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.
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");});
}
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
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.
Related
First, sorry for yet another "why my async action hangs" question but I believe this question is different enough.
Surveying dozens of similar questions, the problem of async action deadlock is either in locking yourself out (.Result), using limited resources or using library components incorrectly (web requests seems popular). In the following example, I cannot find any from above:
private async Task ExecuteAsync(Task<int> task)
{
// entering on current thread, that is the main UI thread
await task // execute "task" asynchronnously (on a different thread)
.ConfigureAwait(false); // when done, no need to return to main thread
MessageBox.Show("success"); // succes indicator
}
public MainWindow() //wpf window ctor
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var task = new Task<int>(() => 42); // make an Action wrapping sychronnous method
// fire and forget, never caring for .Result, disard even the task
var _ = ExecuteAsync(task).ConfigureAwait(false);
}
I have commented the sample with my best try on exaplaining how things (should) work, but something in my explanation must be wrong. Even though MainWindow ctor does not deadlock, the Action () => 42 is never executed and "success" message is not shown. After some debugging I managed to fix the sample (using Task.FromResult), but I am still not sure what is wrong with it as it is now and even more importantly why.
What is the error in my reasoning and why was the action never executed/finished?
You did not start the task! You only declared it. Simply awaiting it does not "fire" it.
private async Task ExecuteAsync(Task<int> task)
{
// at first do start the task
task.Start();
await task.ConfigureAwait(false);
MessageBox.Show("success");
}
Note that ConfigureAwait(false) does not guarantee that execution will be continued on a different thread. It only says that you don't need it to be resumed on the original thread. And resuming UI work (like MessageBox.Show()) on a non-UI thread is not recommended.
As NineBerry pointed out, if you want to wrap a synchronous method and let it run on a different thread, you should use Task.Run():
var task = Task.Run(() => YourSynchronousCall());
I'm writing some basic Firebase code in a Xamarin iOS app and am running into a classic deadlock situation with a TaskCompletionSource.
public Task<string> GetUsers()
{
var tcs = new TaskCompletionSource<string>();
_instance.GetChild("users").ObserveSingleEvent(DataEventType.Value,
x => { tcs.SetResult(x); });
return tcs.Task;
}
When I block on this code like so:
var users = GetUsers().Result;
The application deadlocks.
If I understand correctly, the callback is trying to run on the same context that the .Result is waiting on.
What I don't understand is that if I modify my code to await the GetUsers() call in a Task like so:
var result = Task.Run(
async () => await AppContext.Database.GetUsers().ConfigureAwait(false)
).Result;
It still deadlocks.
What's going on here in the second case? Shouldn't the fact that the code is running on another thread due to the Task.Run mean that the outside .Result doesn't block the callback invocation?
EDIT:
Following up on Nkosi's comment I'm asking this because I'm curious as to why the code is blocking. If I await the call
var users = await GetUsers().ConfigureAwait(false);
then the deadlock goes away. I'd just like to understand why it blocks when wrapped in a Task because based on my (clearly incorrect) understanding of Task.Run, it shouldn't.
ObserveSingleEvent always dispatches callback to UI thread (and I think all or almost all firebase callbacks do that). It does not capture synhronization context or something like that - just always dispatches callback to UI thread (remember - it's just a wrapper around native IOS code). So when you block your UI thread by waiting on Result - it will deadlock for obvious reasons, regardless of from which thread you call GetUsers. Links you mention describe another situation when called code captures current synhronization context, so they call that code from background thread which has no synhronization context and callbacks will not be posted to it. That's not the case here.
Given the following method as example:
private async void F()
{
button.IsEnabled = false;
await Task.Delay(1000);
button.IsEnabled = true;
}
In this case, any code starting at await always occurs on another thread (edit: wrong) which presumably should not have access to the UI thread, similarly to desktop apps. In a similar situation, I recall having an exception such as:
The application called an interface that was marshalled for a different thread.
However, the example does not trigger any exception. Is this expected? Can I reliably write code like this?
any code starting at await always occurs on another thread (non-UI thread, right?),
No, not at all. await does not kick off other threads. I have an async intro that may help if you find this statement confusing.
What await will do is schedule the remainder of the method as a continuation to be run after the asynchronous operation completes (in this case, the asynchronous operation is just a timer firing). By default, await will capture a "context", which is SynchronizationContext.Current (or, if it is null, the context is TaskScheduler.Current). In this case, there's a UI SynchronizationContext that ensures the remainder of the async method will run on the UI thread.
Code running on the UI thread has a SynchronizationContext. You can see that by printing SynchronizationContext.Current. Before you await something that context is captured and after the await your code resumes on that context which makes sure the continuation runs on the UI thread.
To get the behavior you're referencing, where the continuation is run on a ThreadPool thread you can disable the SynchronizationContext capturing by using ConfigureAwait(false):
private async void FooAsync()
{
button.IsEnabled = false;
await Task.Delay(1000).ConfigureAwait(false);
button.IsEnabled = true;
}
This code will raise the exception you expect.
Is this expected? Can I reliably write code like this?
Yes and yes. Code using async-await will "do the right thing" by default. But if you do want to offload something to a ThreadPool thread you can use Task.Run.
In the example below two await calls are used. To gain performance, the sample gets converted Task.WaitAll() instead (not really any faster, but this is just an example).
This is code from a library using Sqlite.Net on Android and the method gets called from OnResume() on the main UI thread:
public async Task SetupDatabaseAsync()
{
await CreateTableAsync<Session>();
await CreateTableAsync<Speaker>();
}
Here's the alternative:
public void SetupDatabaseAsync()
{
var t1 = CreateTableAsync<Session>();
var t2 = CreateTableAsync<Speaker>();
Task.WaitAll(t1, t2);
}
But from my understanding Task.WaitAll() should block the UI thread while waiting, thus leading to a deadlock. But it works just fine. Is that because the two calls don't actually invoke anything on the UI thread?
What's the difference if I use Task.WhenAll() instead? My guess it that it would work even if the UI thread would be invoked, just like with await.
I describe the details of the deadlock situation on my blog. I also have an MSDN article on SynchronizationContext that you may find helpful.
In summary, Task.WaitAll will deadlock in your scenario, but only if the tasks need to sync back to the UI thread in order to complete. You can conclude that CreateTableAsync<T>() does not sync back to the UI thread.
In contrast, this code will deadlock:
public async Task SetupDatabaseAsync()
{
await CreateTableAsync<Session>();
await CreateTableAsync<Speaker>();
}
Task.WaitAll(SetupDatabaseAsync());
I recommend that you not block on asynchronous code; in the async world, sync'ing back to the context is the default behavior (as I describe in my async intro), so it's easy to accidentally do it. Some changes to Sqlite.Net in the future may (accidentally) sync back to the original context, and then any code using Task.WaitAll like your original example will suddenly deadlock.
It's best to use async "all the way":
public Task SetupDatabaseAsync()
{
var t1 = CreateTableAsync<Session>();
var t2 = CreateTableAsync<Speaker>();
return Task.WhenAll(t1, t2);
}
"Async all the way" is one of the guidelines I recommend in my asynchronous best practices article.
When you're blocking the UI thread (and the current synchronization context) it will only cause a deadlock if one of the tasks that you're waiting on marshals a delegate to the current context and then waits on it (synchronously or asynchronously). Synchronously blocking on any async method isn't an instant deadlock in every single case.
Because async methods will, by default, marshal the remainder of the method to the current synchronization context and after every single await, and because the task will never finish until that happens, it means that synchronously waiting on methods that use async/await will often deadlock; at least unless the described behavior is explicitly overridden (through, say ConfigureAwait(false)).
Using WhenAll means that you're not blocking the current synchronization context. Instead of blocking the thread you're just scheduling another continuation to be run when all of the other tasks finish, leaving the context free to handle any other requests that are ready right now (like, say, the continuation from the underlying async method that WhenAll is waiting on).
Maybe this sample will demonstrate what might be happening. It's an iOS view loading. Try it with both the await call and without it (commented out below). Without any await in the function it will run synchronously and the UI will be blocked.
public async override void ViewDidLoad()
{
base.ViewDidLoad ();
var d1 = Task.Delay (10);
var d2 = Task.Delay (10000);
//await Task.Delay (10);
Task.WaitAll (d1, d2);
this.label.Text = "Tasks have ended - really!";
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear (animated);
this.label.Text = "Tasks have ended - or have they?";
}
Suppose I had and API event, where I had to return a result.
The computation I have to do uses an async method, which creates a control (must be run on the UI thread).
private async void API_QueryControl(object sender, APIEventArgs e)
{
e.Result = await CreateControl();
}
private async Task<Control> CreateControl()
{
await Task.Delay(1000);
return new Panel();
}
Now, this is obviously not going to work, since API_QueryControl yields before I set the result. I have no control over the API, which means the event cannot be changed. How would I get the event to wait for the result?
You can block the current (UI) thread, but avoid a deadlock, by using ConfigureAwait(false). Like this:
e.Result = await CreateControl().ConfigureAwait(false);
ConfigureAwait(false) makes the continuation part of CreateControl - the part after await Task.Delay - run on the threadpool instead of the original UI thread.
I assume you have some reason for using async like this, instead of just writing a synchronous version of CreateControl. That would not only be simpler code, it would also use the original (UI) thread to do the work, instead of using a second thread while the UI thread waits for it.
Also note that you should always prevent exceptions from being thrown from async void methods - see the accepted answer here.