async method freezes UI

2019-09-02 11:47发布

问题:

Expected result:

TestAsync is called by UI thread and a worker thread executes LongTask.

Actual result:

Ui thread executes everything

Test:

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    // [...]

    _fab = root.FindViewById<FloatingActionButton>(...);
    _fab.Click += ((sender, v) =>  TestAsync("fab"));

    // [...]
}

private async void TestAsync(string origin)
{
    await LongTask(); 
}

private async Task LongTask()
{
    while (true) { } // Thread should hung here
}

Outcome: The Ui freezes.

Test 2: In order to make sure the UI is executing everything, I made a network operation instead (which is not allowed in the UI thread in Android)

public async Task<int> Network(string s)
{
    URL url = new URL("http://www.randomtext.me/api/");
    Java.IO.BufferedReader reader = new Java.IO.BufferedReader(new Java.IO.InputStreamReader(url.OpenStream()));

    int count = 0;
    string str;
    while ((str = reader.ReadLine()) != null) {
        count += str.Length;
    }
    reader.Close();

    await Task.Delay(3000); // To make sure this method is compiled as async even though it isn't necessary

    return count;
}

Outcome: NetworkOnMainThreadException.

Question:

Why aren't LongTask nor Network methods executed in a worker thread ? What are await/async for then ?

Thanks.

回答1:

and a worker thread executes LongTask.

No, that won't happen by itself. You await from the GUI thread and so you will block it. This pattern is OK for async I/O because that will free up the Thread.

But when your case is CPU bound, there is no use for async/await, use Task.Run:

private void TestAsync(string origin)
{
    Task.Run( LongTask); 
}


回答2:

Main rule is:

  • ui related behavior must be in main thread (ui thread)
  • networking/heavy tasks must be performed in non main (ui) thread.

You try to perform networking task from main (ui) thread, it caused you got this exception.

Try to create TestAsync class extending AsyncTask class. Then override doInBackground, onPostExecute methods.

  • Make your networking logic in doInBackground method.
  • Then update your UI in onPostExecute method.

Reference on this: http://programmerguru.com/android-tutorial/what-is-asynctask-in-android/



回答3:

Task one: async/await

private async void TestAsync(string origin)
{
    await LongTask(); 
}

Explanation:

When the buttons click event delegate is invoked, it is invoked from the main dispatcher (UI Thread). It tells the delegate to call TestAsync("Fab") Synchronously. When the delegate runs through the test async method, it is told to run the Task LongTask but you are also telling it to await the outcome of that Task by using the 'await' request. So the TestAsync method cannot complete until the LongTask has been completed; as this is requested from the main dispatcher the rest of the UI hangs until it's completed.

Resolution:

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
    // [...]

    _fab = root.FindViewById<FloatingActionButton>(...);
    _fab.Click += ((sender, v) =>  TestAsync("fab"));

    // [...]
}

private async void TestAsync(string origin)
{
    // By not calling await you tell the code that you don't need this
    // calling method to await a return value.
    // using Task.Run explicitly requests another thread process this request
    Task.Run(LongTask); 
}

private async Task LongTask()
{
    while (true) { } // Thread should hung here
}


Task two: Network

public async Task<int> Network(string s)
{
    URL url = new URL("http://www.randomtext.me/api/");
    Java.IO.BufferedReader reader = new Java.IO.BufferedReader(new Java.IO.InputStreamReader(url.OpenStream()));

    int count = 0;
    string str;
    while ((str = reader.ReadLine()) != null) {
        count += str.Length;
    }
    reader.Close();

    await Task.Delay(3000); // To make sure this method is compiled as async even though it isn't necessary

    return count;
}

Explanation:

As before this largely depends on how the task is being started, see this quote from the Microsoft documentation:

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active. You can use Task.Run to move CPU-bound work to a background thread, but a background thread doesn't help with a process that's just waiting for results to become available.

I hope this answer adds a little more detail to be used alongside the other responses to your question.