Як дізнатися, чи закінчились інші теми?


126

У мене є об'єкт із методом StartDownload(), який називається , який починає три нитки.

Як мені отримати сповіщення, коли кожен потік завершує виконання?

Чи є спосіб дізнатися, чи закінчена одна (або вся) нитка чи все ще виконується?


1
Погляньте на клас «Бар’єр
Fortyrunner

Відповіді:


228

Є кілька способів зробити це:

  1. Використовуйте Thread.join () в основному потоці, щоб зачекати блокуючим способом завершення кожної теми, або
  2. Перевірте Thread.isAlive () в опитувальному порядку - як правило, не рекомендується - чекати, поки кожна нитка завершиться, або
  3. Неортодоксально, для кожної теми, що запитається , зателефонуйте setUncaughtExceptionHandler, щоб викликати метод у вашому об'єкті, і запрограмуйте кожну нитку для викидання невловимого винятку, коли він завершиться, або
  4. Використовуйте блокування чи синхронізатори чи механізми з java.util.concurrent або
  5. Більш ортодоксально, створіть слухача в основній темі, а потім запрограмуйте кожну свою тему, щоб сказати слухачеві, що вони завершили роботу.

Як реалізувати Ідею №5? Ну, один із способів - спочатку створити інтерфейс:

public interface ThreadCompleteListener {
    void notifyOfThreadComplete(final Thread thread);
}

тоді створіть такий клас:

public abstract class NotifyingThread extends Thread {
  private final Set<ThreadCompleteListener> listeners
                   = new CopyOnWriteArraySet<ThreadCompleteListener>();
  public final void addListener(final ThreadCompleteListener listener) {
    listeners.add(listener);
  }
  public final void removeListener(final ThreadCompleteListener listener) {
    listeners.remove(listener);
  }
  private final void notifyListeners() {
    for (ThreadCompleteListener listener : listeners) {
      listener.notifyOfThreadComplete(this);
    }
  }
  @Override
  public final void run() {
    try {
      doRun();
    } finally {
      notifyListeners();
    }
  }
  public abstract void doRun();
}

а потім кожен з ваших потоків буде поширюватися NotifyingThreadі замість реалізації run()вона буде здійснювати doRun(). Таким чином, коли вони завершать, вони автоматично повідомлять усіх, хто чекає сповіщення.

Нарешті, у вашому головному класі - тому, що запускає всі теми (або принаймні об’єкт, який очікує на сповіщення) - модифікуйте цей клас implement ThreadCompleteListenerта одразу після створення кожної теми додайте себе до списку слухачів:

NotifyingThread thread1 = new OneOfYourThreads();
thread1.addListener(this); // add ourselves as a listener
thread1.start();           // Start the Thread

тоді, коли кожна тема notifyOfThreadCompleteбуде завершена , ваш метод буде викликаний з екземпляром Thread, який щойно завершився (або вийшов з ладу).

Зауважте, що краще було б implements Runnableзамість того, extends Threadщоб NotifyingThreadрозширити нитку, як правило, не відволікається на новий код. Але я кодую ваше запитання. Якщо ви змінили NotifyingThreadклас на реалізацію, Runnableвам доведеться змінити частину свого коду, який керує потоками, що досить просто зробити.


Привіт!! Мені подобається остання ідея. Я реалізувати слухача для цього? Спасибі
Рікардо Фелгейраш,

4
але, використовуючи такий підхід, notifiyListeners викликається всередині run (), тому він буде викликаний настійним потоком, і подальші дзвінки будуть здійснені і там, чи не так?
Jordi Puigdellívol

1
@ Eddie Jordi запитував, чи можна викликати notifyметод не insinde runметодом, а після нього.
Томаш Dzięcielewski

1
Питання справді таке: як зараз вимкнути вторинну нитку. Я знаю, що це закінчено, але як я зараз отримую доступ до основної теми?
Патрік

1
Чи безпечна ця нитка? Здається, що notifyListeners (і, таким чином, notifyOfThreadComplete) буде викликано в NotifyingThread, а не в потоці, який створив, створив сам Слухач.
Аарон

13

Розчин за допомогою CyclicBarrier

public class Downloader {
  private CyclicBarrier barrier;
  private final static int NUMBER_OF_DOWNLOADING_THREADS;

  private DownloadingThread extends Thread {
    private final String url;
    public DownloadingThread(String url) {
      super();
      this.url = url;
    }
    @Override
    public void run() {
      barrier.await(); // label1
      download(url);
      barrier.await(); // label2
    }
  }
  public void startDownload() {
    // plus one for the main thread of execution
    barrier = new CyclicBarrier(NUMBER_OF_DOWNLOADING_THREADS + 1); // label0
    for (int i = 0; i < NUMBER_OF_DOWNLOADING_THREADS; i++) {
      new DownloadingThread("http://www.flickr.com/someUser/pic" + i + ".jpg").start();
    }
    barrier.await(); // label3
    displayMessage("Please wait...");
    barrier.await(); // label4
    displayMessage("Finished");
  }
}

label0 - циклічний бар'єр створюється з кількістю сторін, рівній кількості виконуваних потоків плюс один для основного потоку виконання (в якому виконується startDownload ())

мітка 1 - n-а ЗавантаженняThread заходить у зал очікування

мітка 3 - NUMBER_OF_DOWNLOADING_THREADS увійшли до зали очікування. Основний потік виконання звільняє їх від того, щоб почати виконувати завдання з завантаження більш-менш за один і той же час

мітка 4 - головна нитка виконання надходить у зал очікування. Це "найскладніша" частина коду, яку потрібно зрозуміти. Не має значення, яка нитка ввійде в зал очікування вдруге. Важливо, щоб будь-яка нитка входила в приміщення останньою, гарантувала, що всі інші потоки для завантаження закінчили роботу над завантаженням.

мітка 2 - n-а програма DownloadingThread закінчила роботу з завантаження і входить у зал очікування. Якщо він останній, тобто вже введено його NUMBER_OF_DOWNLOADING_THREADS, включаючи основний потік виконання, головний потік продовжить його виконання лише тоді, коли всі інші потоки закінчать завантаження.


9

Ви повинні дійсно віддати перевагу рішення , яке використовує java.util.concurrent. Знайдіть і прочитайте Джоша Блоха та / або Брайана Геца з цієї теми.

Якщо ви не використовуєте java.util.concurrent.*та берете на себе відповідальність за використання join()потоків безпосередньо, то, ймовірно, вам слід скористатися, щоб знати, коли буде виконано нитку. Ось надзвичайно простий механізм зворотного виклику. Спочатку розгорніть Runnableінтерфейс, щоб мати зворотний дзвінок:

public interface CallbackRunnable extends Runnable {
    public void callback();
}

Потім зробіть Виконавця, який виконає ваш запуск і передзвонить вам, коли це буде зроблено.

public class CallbackExecutor implements Executor {

    @Override
    public void execute(final Runnable r) {
        final Thread runner = new Thread(r);
        runner.start();
        if ( r instanceof CallbackRunnable ) {
            // create a thread to perform the callback
            Thread callerbacker = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // block until the running thread is done
                        runner.join();
                        ((CallbackRunnable)r).callback();
                    }
                    catch ( InterruptedException e ) {
                        // someone doesn't want us running. ok, maybe we give up.
                    }
                }
            });
            callerbacker.start();
        }
    }

}

Інший вид очевидної речі, який потрібно додати до вашого CallbackRunnableінтерфейсу, - це спосіб обробляти будь-які винятки, тому, можливо, покладіть public void uncaughtException(Throwable e);рядок туди і у виконавця, встановіть Thread.UncaughtExceptionHandler, щоб надіслати вас до цього методу інтерфейсу.

Але робити все, що насправді починає пахнути java.util.concurrent.Callable. Ви дійсно повинні поглянути на використання, java.util.concurrentякщо ваш проект це дозволяє.


Мені трохи не зрозуміло, що ви отримуєте від цього механізму зворотного виклику проти простого виклику, runner.join()а потім будь-який код, який ви хочете після цього, оскільки ви знаєте, що нитка закінчена. Чи просто ви маєте визначити цей код як властивість запущеного, щоб у вас могли бути різні речі для різних бігунів?
Стівен

2
Так, runner.join()це найбільш прямий спосіб почекати. Я припускав, що ОП не хотіла блокувати їх основний потік дзвінка, оскільки вони просили отримувати "повідомлення" про кожне завантаження, яке може завершитися в будь-якому порядку. Це запропонувало один із способів отримувати сповіщення асинхронно.
broc.seib

4

Ви хочете чекати, коли вони закінчать? Якщо так, використовуйте метод приєднання.

Існує також властивість isAlive, якщо ви просто хочете його перевірити.


3
Зауважте, що isAlive повертає значення false, якщо нитка ще не почала виконуватись (навіть якщо ваш власний потік вже викликав запуск на ній).
Том Хотін - тайклін

@ TomHawtin-tackline Ви впевнені в цьому? Це суперечить документації Java ("Нитка жива, якщо вона була запущена і ще не загинула" - docs.oracle.com/javase/6/docs/api/java/lang/… ). Тут також суперечить відповідям ( stackoverflow.com/questions/17293304/… )
Стівен

@Stephen З давніх пір я це написав, але, здається, це правда. Я думаю, це спричинило іншим людям проблеми, які були мені свіжими в пам’яті дев'ять років тому. Саме те, що спостерігається, залежатиме від впровадження. Ви розповісти , Threadщоб start, що нитка робить, але виклик повертається негайно. isAliveповинен бути простим тестом на прапор, але коли я google це був метод native.
Том Хотін - тайклін

4

Ви можете допитати екземпляр потоку за допомогою getState (), який повертає екземпляр перерахунку Thread.State з одним із наступних значень:

*  NEW
  A thread that has not yet started is in this state.
* RUNNABLE
  A thread executing in the Java virtual machine is in this state.
* BLOCKED
  A thread that is blocked waiting for a monitor lock is in this state.
* WAITING
  A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
* TIMED_WAITING
  A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
* TERMINATED
  A thread that has exited is in this state.

Однак я думаю, що було б кращим дизайном мати головну нитку, яка чекає, коли 3 дитини закінчать, потім майстер продовжить виконання, коли інші 3 закінчать.


Очікування виходу трьох дітей може не входити в парадигму використання. Якщо це менеджер завантажень, вони можуть захотіти запустити 15 завантажень і просто видалити стан з панелі стану або попередити користувача про завершення завантаження, і в цьому випадку зворотний виклик буде працювати краще.
digitaljoel

3

Ви також можете використовувати Executorsоб'єкт для створення пулу потоків ExecutorService . Потім скористайтеся invokeAllметодом, щоб запустити кожну свою нитку та отримати майбутні ф'ючерси. Це буде заблоковано, поки всі не закінчать виконання. Вашим іншим варіантом буде виконати кожен за допомогою пулу, а потім зателефонувати, awaitTerminationщоб заблокувати, поки пул не буде закінчено виконувати. Просто shutdownдодайте (), коли ви додасте завдання.


2

Багато речей було змінено за останні 6 років на багатонитковому фронті.

Замість використання join()та блокування API ви можете використовувати

1. API ExecutorService invokeAll()

Виконує задані завдання, повертаючи список Futures з їх статусом та результатами, коли всі вони завершені.

2. CountDownLatch

Допомога синхронізації, яка дозволяє одному або більше потокам чекати, поки не завершиться набір операцій, що виконуються в інших потоках.

A CountDownLatchініціалізується із заданим числом. Блокують методи очікування, поки кількість поточного числа не досягне нуля через виклики countDown()методу, після чого всі потоки очікування звільняються та будь-які наступні виклики повернення очікують негайно. Це явище однократне - підрахунок не може бути скинутий. Якщо вам потрібна версія, яка скидає кількість, подумайте про використання CyclicBarrier.

3. ForkJoinPool або newWorkStealingPool()в виконавців інший шлях

4.Переводити всі Futureзавдання від подачі ExecutorServiceі перевіряти стан з блокуванням виклику get()на Futureоб'єкті

Подивіться на відповідні питання SE:

Як чекати, коли нитка, яка породжує власну нитку?

Виконавці: Як синхронно дочекатися завершення всіх завдань, якщо завдання створюються рекурсивно?


2

Я б запропонував поглянути на javadoc для класу Thread .

У вас є кілька механізмів маніпулювання нитками.

  • Ваша основна нитка могла б join()три потоки послідовно, і тоді вона не продовжуватиметься, поки всі три не будуть виконані.

  • Опитуйте стан ниток породжених ниток з інтервалом.

  • Покладіть всі породжені нитки в окремий ThreadGroupі опитуйте activeCount()на ThreadGroupтечії та зачекайте, поки вона досягне 0.

  • Налаштуйте користувальницький тип інтерфейсу для зворотного дзвінка або прослуховувача для міжпотокового зв'язку.

Я впевнений, що існує багато інших способів, яких я все ще пропускаю.


1

Ось таке рішення, яке є простим, коротким, легким для розуміння і прекрасно працює для мене. Мені потрібно було намалювати на екрані, коли закінчується інша нитка; але не вдалося, оскільки головна нитка контролює екран. Так:

(1) Я створив глобальну змінну: boolean end1 = false;потік встановлює її як істинну при закінченні. Це підхоплюється в основному потоці циклом "postDelayed", де він відповідає.

(2) Моя нитка містить:

void myThread() {
    end1 = false;
    new CountDownTimer(((60000, 1000) { // milliseconds for onFinish, onTick
        public void onFinish()
        {
            // do stuff here once at end of time.
            end1 = true; // signal that the thread has ended.
        }
        public void onTick(long millisUntilFinished)
        {
          // do stuff here repeatedly.
        }
    }.start();

}

(3) На щастя, "postDelayed" працює в основній нитці, тож саме тут слід перевіряти інший потік раз на секунду. Коли інша нитка закінчиться, це може початися все, що ми хочемо зробити далі.

Handler h1 = new Handler();

private void checkThread() {
   h1.postDelayed(new Runnable() {
      public void run() {
         if (end1)
            // resond to the second thread ending here.
         else
            h1.postDelayed(this, 1000);
      }
   }, 1000);
}

(4) Нарешті, почніть все, що працює десь у вашому коді, зателефонувавши:

void startThread()
{
   myThread();
   checkThread();
}

1

Я думаю, найпростіший спосіб - це використовувати ThreadPoolExecutorклас.

  1. У ньому є черга, і ви можете встановити, скільки ниток має працювати паралельно.
  2. У ньому є приємні методи зворотного виклику:

Гакові методи

Цей клас забезпечує захищені перезаписи beforeExecute(java.lang.Thread, java.lang.Runnable)та afterExecute(java.lang.Runnable, java.lang.Throwable)методи, які викликаються до та після виконання кожного завдання. Вони можуть бути використані для маніпулювання середовищем виконання; наприклад, повторна ініціалізація ThreadLocals, збір статистики або додавання записів журналу. Крім того, метод terminated()може бути замінений для виконання будь-якої спеціальної обробки, яку необхідно виконати після повного припинення Виконавця.

що саме те, що нам потрібно. Ми будемо заміняти afterExecute()зворотні виклики після того, як буде виконано кожен потік, і будемо заміняти, terminated()щоб знати, коли всі потоки виконані.

Тож ось що ви повинні зробити

  1. Створіть виконавця:

    private ThreadPoolExecutor executor;
    private int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();    
    
    
    
    private void initExecutor() {
    
    executor = new ThreadPoolExecutor(
            NUMBER_OF_CORES * 2,  //core pool size
            NUMBER_OF_CORES * 2, //max pool size
            60L, //keep aive time
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>()
    ) {
    
        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
                //Yet another thread is finished:
                informUiAboutProgress(executor.getCompletedTaskCount(), listOfUrisToProcess.size());
            }
        }
    
    };
    
        @Override
        protected void terminated() {
            super.terminated();
            informUiThatWeAreDone();
        }
    
    }
  2. І починайте свої теми:

    private void startTheWork(){
        for (Uri uri : listOfUrisToProcess) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    doSomeHeavyWork(uri);
                }
            });
        }
        executor.shutdown(); //call it when you won't add jobs anymore 
    }

Внутрішнім методом informUiThatWeAreDone();зробіть все, що вам потрібно зробити, коли всі потоки виконані, наприклад, оновіть інтерфейс користувача.

Примітка: Не забувайте про використання synchronizedметодів , так як ви робите свою роботу паралельно і бути дуже обережними , якщо ви вирішите виклик synchronizedметод з іншого synchronizedметоду! Це часто призводить до тупиків

Сподіваюся, це допомагає!



0

Подивіться на документацію Java для класу Thread. Ви можете перевірити стан потоку. Якщо розмістити три потоки в змінних членів, то всі три потоки можуть читати стани один одного.

Ти повинен бути трохи обережним, тому що ти можеш викликати перегони між потоками. Просто спробуйте уникати складної логіки, заснованої на стані інших потоків. Однозначно уникайте запису декількох потоків до одних і тих же змінних.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.