Assume Executor()
is running on the UI thread.
void Executor()
{
BadAsync();
Thread.Sleep(1000); // E1
Thread.Sleep(1000); // E2
}
async void BadAsync()
{
await Task.Delay(1000); // B1
Thread.Sleep(1000); // B2
}
Executor()
does not await BadAsync()
then B1
and E1
run simultaneously in the first second.B1
captures the UI thread then B2
runs on the UI thread too.x
and y
in the UI thread for B2
and E2
.Can we know which time slot will be occupied by
B2
?
Using a minimal example with MAUI.
<VerticalStackLayout>
<Label x:Name="start" Text="Start"/>
<Label x:Name="stop" Text="Stop"/>
</VerticalStackLayout>
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
start.Text = DateTime.UtcNow.ToLongTimeString();
}
protected override async void OnAppearing() // analogous to BadAsync()
{
var d = 979; // in my computer 979 is the average of critical values
await Task.Delay(d);
Thread.Sleep(5000 - d);
stop.Text = DateTime.UtcNow.ToLongTimeString();
}
}
// Page.cs
[EditorBrowsable(EditorBrowsableState.Never)]
public void SendAppearing()// analogous to Executor()
{
// others ...
OnAppearing();
Appearing?.Invoke(this, EventArgs.Empty);
// others ...
}
When d
is set to a value far below 979, the output starts with a blank window first and then switch to the following after several milliseconds.
In this case B2
is executed in SendAppearing()
(or before SendAppearing()
returns).
But when d
is set to a value far above 979, the output starts with
first and then
after several milliseconds.
In this case B2
is executed outside SendAppearing()
(or after SendAppearing()
returns).
public partial class MainPage : ContentPage
{
public MainPage(MainPageViewModel model)
{
InitializeComponent();
BindingContext = model;
}
protected override async void OnAppearing() // analogous to BadAsync()
{
if(BindingContext is MainPageViewModel model)
{
await model.LoadDataAsync();
UpdateUISync();
}
}
}
B2 will run after E2.
Task.Delegate is just a wrapper around System.Threading.Timer. So your example would be converted to something like this:
void Executor()
{
BadAsync();
Thread.Sleep(1000); // E1
Thread.Sleep(1000); // E2
}
SynchronizationContext context;
System.Threading.Timer timer;
void BadAsync()
{
context = SynchronizationContext.Current;
timer = new System.Threading.Timer(OnTimerElapsed, null, 1000, Timeout.Infinite );
}
void OnTimerElapsed(object? _){
timer.Dispose();
if(context != null){
context.Post(B2);
}
else{
B2();
}
}
void B2(){
Thread.Sleep(1000); // B2
}
Lets say we have threads available, so OnTimerElapsed
will run 1s after the timer started. If Executor
was called on the UI thread then context.Post(B2)
will be called while the UI thread is sleeping in E2.
context.Post
essentially just adds a message to a thread safe queue (the message queue) that the UI thread reads from. But if the UI thread is bussy (by sleeping) it cannot process any messages, so it first has to finish whatever it is doing, return to the message loop, process any other messages, and then finally run B2
when its message is processed.
This is one reason you should avoid running anything time consuming on the UI thread. If messages are not being processed the application will appear "frozen". So do not block the UI thread.
You usually want to be explicit in the order you run things in, by ensuring all async methods return tasks, and all tasks are awaited. This also helps ensures failures are handled. The main exception are event handlers, where you should make sure to handle any exceptions.