11

The title may be a bit misleading, my question is more about why it works in this weird way.

So I have an activity with a layout that has a TextView and a ListView. I have a long running async method that prepares data to be displayed in the list. So the initial code is like this:

    protected async override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.MyView);
        await SetupData();
    }

    private async Task SetupData(){
        Task.Run(async () => {
            var data = await new SlowDataLoader().LoadDataAsync();
            // For simplicity setting data on the adapter is omitted here
        });
    }

It works, in a sense that it executes without errors. However, the activity appears as a blank screen, and even the text view only renders after a certain delay. So it appears that task is actually not running asynchronously. Setting ConfigureAwait(false) on both "await" calls didn't help. Moving the SetupData() call into OnPostCreate, OnResume and OnPostResume has no effect. The only thing that made the TextView appear immediately and render the list later, when data arrived is this:

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.MyView);
        new Handler().PostDelayed(async ()=>{
            await SetupData();
        }, 100);
    }

So the question is, why doesn't

await SetupData().ConfigureAwait(false); 

unblock the flow? Why do we have to force delay the start of the async operation to let UI finish rendering, even though (according to this http://www.wintellect.com/devcenter/paulballard/tasks-are-still-not-threads-and-async-is-not-parallel) SetupData is supposed to be able to run as a separate thread here ?

p.s. removing the code that sets data on the adapter doesn't affect this behavior - there is still a delay before the screen is rendered. So I'm not showing that code here.

SushiHangover
  • 73,120
  • 10
  • 106
  • 165
Dennis K
  • 1,828
  • 1
  • 16
  • 27

3 Answers3

20

By awaiting within the UI Looper, you are blocking further code execution on the thread while your SetupData method runs.

Non-blocking Example:

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        SetContentView(Resource.Layout.Main);
        Task.Run(() => SetupData());
        Console.WriteLine("UI Thread / Message Looper is not blocked");
    }

    void SetupData()
    {
        Task.Run(async () =>
        {
            Console.WriteLine($"Are we on the UI thread? {Looper.MainLooper.Thread == Looper.MyLooper()?.Thread}");
            // Simulate a long running task
            await Task.Delay(TimeSpan.FromSeconds(10));
            Console.WriteLine("Done fetching/calculating data");
            RunOnUiThread(() =>
            {
                // Update the data fetched/calculated on the UI thread;
                Console.WriteLine($"Are we on the UI thread? {Looper.MainLooper.Thread == Looper.MyLooper().Thread}");
            });
        }).Wait();
        Console.WriteLine("Done w/ SetupData");
    }

Output:

UI Thread / Message Looper is not blocked
Are we on the UI thread? False
Done fetching/calculating data
Are we on the UI thread? True
Done w/ SetupData
SushiHangover
  • 73,120
  • 10
  • 106
  • 165
  • This is it! Actually simply removing await from OnCreate in the original code fixes it (i.e. no need to call SetupData from a Task). I still need to comprehend why this is happening (I thought when await is called, the awaited operation is started in a different task, but the method exits immediately and lets the thread continue). But this is an excellent answer pointing to the root of the problem. TL;DR = don't await in Activity callbacks. – Dennis K Feb 02 '17 at 19:20
  • @DennisK The await**ed** operation is running on a different thread since you used `Task.Run`, *but* the method, and thus the thread, that is await**ing** has been suspended till the await**ed** operation returns. – SushiHangover Feb 02 '17 at 19:24
  • I'm sure you're right, but still can't wrap my head around it. This is what confuses me: https://msdn.microsoft.com/en-us/library/mt674882.aspx . "An await expression in an async method doesn’t block the current thread while the awaited task is running. Instead, the expression signs up the rest of the method as a continuation and returns control to the caller of the async method.". It specifically says "await doesn't block the current thread", so I was expecting OnCreate to return control to the caller.. – Dennis K Feb 02 '17 at 19:45
  • I think I found the real problem which lied a bit deeper down the path you highlighted. I'll keep your answer as accepted, but add my own for completeness. (BTW, it's better to put the "UI Thread / Message Looper is not blocked" message into OnResume rather than at the end of OnCreate for proper indication of unblock. It gets called even if OnCreate is suspended) – Dennis K Feb 02 '17 at 21:22
1

To complement the answer from @SushiHangover, I'm adding my own answer to point out the actual bug and list possible solutions in addition to the one suggested by @SushiHangover.

Consider the Example at the very bottom of this page https://msdn.microsoft.com/en-us/library/hh156528.aspx

The real problem in the original code (and all other variants I tried) was that even though SetupData was declared as async method, it was actually running as synchronous. So when OnCreate was awaiting on a synchronous method, it was blocking (exactly what they demo in the Example above). This issue can be corrected via several ways. First, as SushiHangover suggested, do not await on this method, and since it's sync, call it as such (and may as well remove async keyword and return void from it).

Another approach, which may be more suitable in some situations, is to await on the Task that's created inside that method:

private async Task SetupData(){
    await Task.Run(async () => {
        var data = await new SlowDataLoader().LoadDataAsync();
        // For simplicity setting data on the adapter is omitted here
    });
}

Or, change this method to comply with async method requirements by returning the task:

private Task SetupData(){
    return Task.Run(async () => {
        var data = await new SlowDataLoader().LoadDataAsync();
        // For simplicity setting data on the adapter is omitted here
    });
}

Both these changes allow the await in OnCreate work as expected - OnCreate method exits, while data is still being loaded.

Dennis K
  • 1,828
  • 1
  • 16
  • 27
  • Did this really work ? If you manipulated UI (such as the adapter you mentioned), I would expect a `CalledFromWrongThreadException`, since you modify `View`s from a worker thread. – JonZarate Apr 30 '18 at 11:39
-1

Because it is not running on UI Thread this can help you get a clearer view What is the Android UiThread (UI thread)

Community
  • 1
  • 1
Mike
  • 1,313
  • 12
  • 21
  • Sorry, could you elaborate more? What is not running on the UI thread? The whole point here is to not run the long task on the UI thread. The issue is that UI thread still seems to be blocked. – Dennis K Feb 02 '17 at 17:39