¿Cómo crear un hilo de Looper y luego enviarle un mensaje inmediatamente?

76

Tengo un hilo de trabajo que se encuentra en segundo plano, procesando mensajes. Algo como esto:

class Worker extends Thread {

    public volatile Handler handler; // actually private, of course

    public void run() {
        Looper.prepare();
        mHandler = new Handler() { // the Handler hooks up to the current Thread
            public boolean handleMessage(Message msg) {
                // ...
            }
        };
        Looper.loop();
    }
}

Desde el hilo principal (hilo de la interfaz de usuario, no es que importe) me gustaría hacer algo como esto:

Worker worker = new Worker();
worker.start();
worker.handler.sendMessage(...);

El problema es que esto me prepara para una hermosa condición de carrera: en el momento en que worker.handlerse lee, ¡no hay forma de estar seguro de que el hilo de trabajo ya se haya asignado a este campo!

No puedo simplemente crear el Handlerdesde el Workerconstructor de, porque el constructor se ejecuta en el hilo principal, por lo que Handlerse asociará con el hilo incorrecto.

Esto no parece un escenario poco común. Puedo encontrar varias soluciones, todas feas:

  1. Algo como esto:

    class Worker extends Thread {
    
        public volatile Handler handler; // actually private, of course
    
        public void run() {
            Looper.prepare();
            mHandler = new Handler() { // the Handler hooks up to the current Thread
                public boolean handleMessage(Message msg) {
                    // ...
                }
            };
            notifyAll(); // <- ADDED
            Looper.loop();
        }
    }
    

    Y del hilo principal:

    Worker worker = new Worker();
    worker.start();
    worker.wait(); // <- ADDED
    worker.handler.sendMessage(...);
    

    Pero esto tampoco es confiable: si notifyAll()sucede antes del wait(), ¡nunca nos despertaremos!

  2. Pasando una inicial Messageal Workerconstructor de, haciendo que el run()método la publique. Una solución ad-hoc, no funcionará para varios mensajes, o si no queremos enviarla de inmediato, pero poco después.

  3. Ocupado esperando hasta que el handlercampo ya no esté null. Sí, un último recurso ...

Me gustaría crear un Handlery MessageQueueen nombre del Workerhilo, pero esto no parece ser posible. ¿Cuál es la forma más elegante de salir de esto?

Thomas
fuente
12
¿Alguna razón en particular que no esté usando HandlerThread?
CommonsWare
2
@CommonsWare: Hmm, no sabía que existía. Sin referencias cruzadas en los documentos. Su getLooper()método se bloquea hasta que tenemos un Looper, luego podemos usar new Handler(worker.getLooper()) desde el hilo principal para inicializar el Handler. Eso resolvería el problema, ¿verdad?
Thomas
Creo que sí. OTOH, yo no lo uso mucho, por lo que es posible que me esté perdiendo algo.
CommonsWare
3
@CommonsWare: Resolvió el problema. Ahora, si publicas eso como respuesta, pondré una gran marca de verificación verde al lado;)
Thomas
9
De hecho, creo que sería mejor que lo respondiera usted mismo para explicar cómo HandlerThreadencaja en su Workerpatrón. Al menos, lo explicará mejor de lo que yo podría, ya que era su problema y su implementación de una solución; acabo de señalar una clase de ayuda para abordar el problema.
CommonsWare

Respuestas:

64

Solución eventual (menos verificación de errores), gracias a CommonsWare:

class Worker extends HandlerThread {

    // ...

    public synchronized void waitUntilReady() {
        d_handler = new Handler(getLooper(), d_messageHandler);
    }

}

Y del hilo principal:

Worker worker = new Worker();
worker.start();
worker.waitUntilReady(); // <- ADDED
worker.handler.sendMessage(...);

Esto funciona gracias a la semántica de HandlerThread.getLooper()qué bloques hasta que se inicializa el looper.


Por cierto, esto es similar a mi solución n. ° 1 anterior, ya que HandlerThreadse implementa aproximadamente de la siguiente manera (me encanta el código abierto):

public void run() {
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Looper.loop();
}

public Looper getLooper() {
    synchronized (this) {
        while (mLooper == null) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
    }
    return mLooper;
}

La diferencia clave es que no verifica si el hilo de trabajo se está ejecutando, sino que en realidad ha creado un bucleador; y la forma de hacerlo es almacenar el looper en un campo privado. ¡Agradable!

Thomas
fuente
1
Gracias por esto. Estoy comentando solo para señalar que waitUntilReady () debe llamarse después de worker.start (). Mirando hacia atrás, suena bastante obvio, pero me tomó un poco entender lo que estaba haciendo mal al obtener una excepción de puntero nulo.
fedepaol
4
@Snicolas no puede, porque el controlador está limitado al hilo donde se crea. Inicializarlo en el constructor de HandlerThread daría como resultado un controlador en el hilo principal o donde se crea HandlerThread. Si desea usar el constructor Handler con el looper del Handler como parámetro (el que se usó en esperar hasta que ya), estoy bastante seguro de que resultaría en un punto muerto.
fedepaol
1
Ok, ese es un buen punto. Gracias por publicar. Por lo tanto, para proteger al controlador de un mal uso, puede envolver los métodos del controlador en HandlerThread que podría actuar como una fachada y delegar cada llamada al controlador, después de que se haya inicializado el controlador. Esa sería una mejor manera de encapsularlo y evitar que se olvide waitUntilReady.
Snicolas
2
@Thomas ¿cuál es el propósito principal del d_handler? d_messageHandler maneja los mensajes, ¿verdad? Pero estás enviando mensajes usandowork.handler
woyaru
1
Esto es muy interesante, pero sería más útil si proporcionara algunas notas sobre qué son d_handler y d_messageHandler, y cómo se usan.
RenniePet
1

echa un vistazo al código fuente de HandlerThread

@Override
     public void run() {
         mTid = Process.myTid();
         Looper.prepare();
         synchronized (this) {
             mLooper = Looper.myLooper();
             notifyAll();
         }
         Process.setThreadPriority(mPriority);
         onLooperPrepared();
         Looper.loop();
         mTid = -1;
     }

Básicamente, si está extendiendo Thread en worker e implementando su propio Looper, entonces su clase de thread principal debería extender worker y establecer su controlador allí.

abeja
fuente
1

Estas son mis soluciones: MainActivity:

//Other Code

 mCountDownLatch = new CountDownLatch(1);
        mainApp = this;
        WorkerThread workerThread = new WorkerThread(mCountDownLatch);
        workerThread.start();
        try {
            mCountDownLatch.await();
            Log.i("MsgToWorkerThread", "Worker Thread is up and running. We can send message to it now...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Toast.makeText(this, "Trial run...", Toast.LENGTH_LONG).show();
        Message msg = workerThread.workerThreadHandler.obtainMessage();
        workerThread.workerThreadHandler.sendMessage(msg);

La clase WorkerThread:

public class WorkerThread extends Thread{

    public Handler workerThreadHandler;
    CountDownLatch mLatch;

    public WorkerThread(CountDownLatch latch){

        mLatch = latch;
    }


    public void run() {
        Looper.prepare();
        workerThreadHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {

                Log.i("MsgToWorkerThread", "Message received from UI thread...");
                        MainActivity.getMainApp().runOnUiThread(new Runnable() {

                            @Override
                            public void run() {
                                Toast.makeText(MainActivity.getMainApp().getApplicationContext(), "Message received in worker thread from UI thread", Toast.LENGTH_LONG).show();
                                //Log.i("MsgToWorkerThread", "Message received from UI thread...");
                            }
                        });

            }

        };
        Log.i("MsgToWorkerThread", "Worker thread ready...");
        mLatch.countDown();
        Looper.loop();
    }
}
somenath mukhopadhyay
fuente
0
    class WorkerThread extends Thread {
            private Exchanger<Void> mStartExchanger = new Exchanger<Void>();
            private Handler mHandler;
            public Handler getHandler() {
                    return mHandler;
            }
            @Override
            public void run() {
                    Looper.prepare();
                    mHandler = new Handler();
                    try {
                            mStartExchanger.exchange(null);
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                    }
                    Looper.loop();
            }

            @Override
            public synchronized void start() {
                    super.start();
                    try {
                            mStartExchanger.exchange(null);
                    } catch (InterruptedException e) {
                            e.printStackTrace();
                    }
            }
    }
18446744073709551615
fuente