J'ai un thread de travail qui se trouve en arrière-plan et traite les messages. Quelque chose comme ça:
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();
}
}
À partir du thread principal (thread d'interface utilisateur, ce n'est pas important), je voudrais faire quelque chose comme ceci:
Worker worker = new Worker();
worker.start();
worker.handler.sendMessage(...);
Le problème est que cela me met en place pour une belle condition de concurrence: au moment où worker.handler
Est lu, il n'y a aucun moyen d'être sûr que le thread de travail a déjà été affecté à ce champ!
Je ne peux pas simplement créer le Handler
à partir du constructeur de Worker
, car le constructeur s'exécute sur le thread principal, donc le Handler
s'associera au mauvais thread.
Cela ne semble guère être un scénario inhabituel. Je peux trouver plusieurs solutions de contournement, toutes laides:
Quelque chose comme ça:
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();
}
}
Et à partir du fil principal:
Worker worker = new Worker();
worker.start();
worker.wait(); // <- ADDED
worker.handler.sendMessage(...);
Mais ce n'est pas fiable non plus: si la notifyAll()
se produit avant la wait()
, alors nous ne serons jamais réveillés!
Passer un Message
initial au constructeur de Worker
, avec la méthode run()
le poster. Une solution ad-hoc, ne fonctionnera pas pour plusieurs messages, ou si nous ne voulons pas l'envoyer tout de suite mais peu de temps après.
Attente occupée jusqu'à ce que le champ handler
ne soit plus null
. Oui, un dernier recours ...
Je voudrais créer un Handler
et MessageQueue
au nom du thread Worker
, mais cela ne semble pas être possible. Quelle est la manière la plus élégante de s'en sortir?
Solution éventuelle (vérification des erreurs moins), grâce à CommonsWare:
class Worker extends HandlerThread {
// ...
public synchronized void waitUntilReady() {
d_handler = new Handler(getLooper(), d_messageHandler);
}
}
Et à partir du fil principal:
Worker worker = new Worker();
worker.start();
worker.waitUntilReady(); // <- ADDED
worker.handler.sendMessage(...);
Cela fonctionne grâce à la sémantique de HandlerThread.getLooper()
qui bloque jusqu'à ce que le looper ait été initialisé.
Soit dit en passant, cela est similaire à ma solution n ° 1 ci-dessus, car le HandlerThread
est implémenté à peu près comme suit (je dois aimer l'open source):
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 principale différence est qu'il ne vérifie pas si le thread de travail est en cours d'exécution, mais qu'il a effectivement créé un looper; et la façon de le faire est de stocker le boucleur dans un champ privé. Agréable!
Voici mes solutions: 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 classe 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();
}
}
jetez un oeil au code source 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;
}
Fondamentalement, si vous étendez Thread in worker et implémentez votre propre Looper, votre classe de thread principale doit étendre worker et y définir votre gestionnaire.
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();
}
}
}