Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Runnable.wait() in AsyncTask? Why does the AsyncTask NOT wait...?

I am using AsyncTask to run a background operation. Of course switching to another thread while already working in a background thread does not make a lot of sense in general, except the other thread is the UI thread. This what I would like to to: While the task is running I need to "access" the UI, e.g. to show a dialog to ask the user how to proceed.

  • run the background task
  • stop the task at some point to get user feedback
  • switch to the UI thread to show dialog and ask for input
  • switch back to background task and continue work

How can this be done? I thought I could use Runnable with myActivity.runOnUiThread(runnable) but this does not work:

private void copyFiles() {
    CopyTask copyTask = new CopyTask(this);
    copyTask.execute();
}

// CustomAsyncTask is a AsyncTask subclass that takes a reference to the current
// activity as parameter
private class CopyTask extends CustomAsyncTask<Void, Void, Void> {
    private doCopy;

    @Override
    protected Boolean doInBackground(Void... params) {              
        // Custom code, e.g. copy files from A to B and check for conflict
        for (File file : allFiles) {
            doCopy = true;

            if (isConflict(file)) {
                // Stop current thread and ask for user feedback on UI Thread

                Runnable uiRunnable = new Runnable() {
                    public void run() {
                        // Pos 1. --> Execute custom code, e.g. use AlertDialog to ask user if file should be replaced...
                        doCopy = false;

                        synchronized (this) {
                           this.notify();
                        }
                    }
                });  

                synchronized(uiRunnable) {
                    // Execute code on UI thread
                    activity.runOnUiThread(uiRunnable);

                    // Wait until runnable finished
                    try {
                        uiRunnable.wait();
                    }
                    catch (InterruptedException e ) {
                        e.printStackTrace();
                    }
                }
            }

            // Pos 2. --> Continue work
            if (doCopy)
               copyFromAToB(File);
        }

        return null;
    }
}

Within doInBackground() (--> in a background thread) the AsyncTask calls activity.runOnUiThread(uiRunnable). Next uiRunnable.wait() is called. Regarding to the docu wait() should do the following:

Causes the calling thread to wait until another thread calls the notify() or notifyAll() method of this object.

Thus the background thread should wait to continue its work until this.notify() (== uiRunnable.notifiy()) is called on another thread (= the UI thread), shouldn't it?

Well, id does not wait! After calling uiRunnable.wait() the background thread immediately continues by jumping to if (doCopy).... It seems that the background thread and the main thread are executed in parallel (not surprising since this is what thread do...) and thus its a race condition whether doCopy = false on the UI thread or if (doCopy) on the background thread is reached first.

How is this possible? Why doesn't wait() works as described? Or am I getting something wrong?

Thank you very much!

EDIT: To avoid missunderstandings: Of course I know the lifecycle methodes of AsyncTask but as far as I understand them, they are not what I am looking for (see my reply to the comment blow).

Interrupting the AsyncTask as soon as a UI interaction is necessary, query the UI and start a new AsyncTask would be possible of course. However this would result in code which is very hard to read/understand/maintain.

As I understand the docu of wait() everything should work fine here. Primary question is not how to do UI interaction during the lifecycle of an AsyncTask but why wait()does not work as expected.

like image 729
Andrei Herford Avatar asked Feb 12 '23 14:02

Andrei Herford


1 Answers

The Basics

When you start an AsyncTask first the onPreExecute() method runs on the UI thread. You can override this method to make changes to the UI prior to the doInBackground() method running.

After the doInBackground() method finishes, the onPostExecute() method runs on the UI thread, so you can use this to make changes to the UI from here. If you need to make regular changes to the UI Thread during the doInBackground() method you override the onProgressUpdate() method which runs on the UI Thread, and then call it from within doInBackground(), which will allow you to periodically update the UI.

You could use something like the following;

private class DoStuffTask extends AsyncTask {

   @Override
   protected void doInBackground(Object... args) {
       // Do stuff
       onProgressUpdate(x);
       // Do more stuff
   }

   @Override
   protected void onProgressUpdate(Object... args) {
       // Update your UI here
   }
}

Now if this doesn't quite do it and you want the AsyncTask to wait for input during the doInBackground() method it is probably worth considering using multiple AsyncTasks instead. You can then finish each AsyncTask, ask for input, and then start a new AsyncTask to continue working.

Given that AlertDialog instances are asynchronous, this is probably the preferred solution because you can start the next AsyncTask from the AlertDialog itself.

Using wait() in an AsyncTask

If you would prefer to use a single AsyncTask you can use wait from within your AsyncTask to prevent execution continuing until some condition is met. Instead of using a new Runnable we are just using two threads in this instance, the thread running doInBackground() and the main thread, and we are synchronizing on the AsycTask itself.

Example below;

public class TestTask extends AsyncTask{

    private boolean notified;
    private Promptable p;
    public interface Promptable { public abstract void prompt(); }

    public TestTask(Promptable p){
        this.p = p;
    }

    @Override
    protected Object doInBackground(Object... arg0) {
        Log.d("First", "First");
        onProgressUpdate(null);
        synchronized(this){
            while(!notified){
                try{
                    this.wait();
                }
                catch(InterruptedException e){ }                    
            }
        }
        Log.d("Second", "Second");
        return null;
    }

    @Override 
    protected void onProgressUpdate(Object... args){       
        synchronized(this){
            notified = true;
            p.prompt();               
            this.notify();
        }
    }
}

In the example above, assume that your Activity is parsed into the AsyncTask's constructor, and that it implements an interface we create called Promptable. You'll notice that even though we're calling wait() we are putting it in a while loop. If we didn't do this, and somehow notify() got called before wait() then your thread would lock up indefinitely. Also, you can't depend on the fact that your thread will wait forever, so the while loop ensures that it doesn't continue until notify is called.

I hope this helps.

like image 98
Rudi Kershaw Avatar answered Feb 15 '23 11:02

Rudi Kershaw