Search code examples
c#asynchronousasync-awaitmaui

Can we know which time slot will be occupied by task continuation?


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
}
  • As Executor() does not await BadAsync() then B1 and E1 run simultaneously in the first second.
  • As B1 captures the UI thread then B2 runs on the UI thread too.
  • The following diagram shows there are 2 time slots x and y in the UI thread for B2 and E2.

enter image description hereCan we know which time slot will be occupied by B2?

Edit

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.

    enter image description here

    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

    enter image description here

    first and then

    enter image description here

    after several milliseconds.

    In this case B2 is executed outside SendAppearing() (or after SendAppearing() returns).

Non-trivial example

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();              
        }
    }
}

Solution

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