Які основні напрямки використання урожайності (), і чим він відрізняється від приєднання () та переривання ()?


106

Я трохи заплутаний у використанні yield()методу на Java, зокрема у наведеному нижче прикладі коду. Я також читав, що return () використовується "для запобігання виконанню потоку".

Мої запитання:

  1. Я вважаю, що наведений нижче код призводить до того ж результату як при використанні, так yield()і при його використанні. Це правильно?

  2. Назвіть основні напрями використання yield()?

  3. Чим yield()відрізняється від методів join()і interrupt()методів?

Приклад коду:

public class MyRunnable implements Runnable {

   public static void main(String[] args) {
      Thread t = new Thread(new MyRunnable());
      t.start();

      for(int i=0; i<5; i++) {
          System.out.println("Inside main");
      }
   }

   public void run() {
      for(int i=0; i<5; i++) {
          System.out.println("Inside run");
          Thread.yield();
      }
   }
}

Я отримую той самий вихід, використовуючи код вище, як і без використання yield():

Inside main
Inside main
Inside main
Inside main
Inside main
Inside run
Inside run
Inside run
Inside run
Inside run

Це збіднення слід закрити, щоб бути занадто широким .
Raedwald

Ні. Це не повертає того самого результату, коли у вас є yield()і ні. коли у вас великий i, а не 5, ви можете бачити ефект yield()методу.
Лакшман

Відповіді:


97

Джерело: http://www.javamex.com/tutorials/threads/yield.shtml

Windows

У реалізації точки доступу, спосіб Thread.yield()роботи змінився між Java 5 та Java 6.

У Java 5 Thread.yield()викликає виклик API API Sleep(0). Це має особливий ефект очищення кванту поточної нитки та розміщення її в кінці черги для її рівня пріоритетності . Іншими словами, всі потоки, які можна виконати з однаковим пріоритетом (і ті, що мають більший пріоритет), отримають шанс запуститись до наступного потокового потоку наступного часу, заданого процесором. Коли він врешті-решт буде перепланований, він повернеться з повним повним квантом , але не «перенесе» жодного з кванту, що залишився з моменту отримання. Така поведінка трохи відрізняється від ненульового сну, коли спляча нитка зазвичай втрачає 1 квантове значення (фактично 1/3 10 або 15 мс).

У Java 6 така поведінка була змінена. Тепер VM Hotspot реалізується Thread.yield()за допомогою SwitchToThread()виклику API Windows . Цей виклик змушує поточний потік відмовитись від свого поточного часового відрізка , але не всього його кванту. Це означає, що залежно від пріоритетів інших потоків вихідна нитка може бути запланована назад на один період переривання пізніше . (Див. Розділ щодо планування потоків для отримання додаткової інформації про часові часові відсіки.)

Linux

У Linux, Hotspot просто телефонує sched_yield(). Наслідки цього дзвінка дещо інші та, можливо, більш серйозні, ніж у Windows:

  • вихідний потік не отримає ще один шматочок процесора, поки всі інші потоки не мали фрагмента процесора ;
  • (принаймні, в ядрі 2.6.8 і далі), той факт, що потік вийшов, неявно враховується евристикою планувальника щодо його недавнього розподілу процесора - таким чином, неявно, потоці, яка вийшла, можна було б отримати більше процесора при плануванні в майбутнє.

(Докладніше про пріоритети та алгоритми планування див. У розділі щодо планування потоків .)

Коли користуватися yield()?

Я б сказав практично ніколи . Його поведінка не є стандартно визначеною, і, як правило, є кращі способи виконання завдань, які, можливо, ви хочете виконати з yield ():

  • якщо ви намагаєтеся використовувати лише частину процесора , ви можете зробити це більш керованим способом, оцінивши, скільки CPU використовує нитка в останньому фрагменті обробки, а потім спати деякий час, щоб компенсувати: див. сну () метод;
  • якщо ви чекаєте, коли процес чи ресурс завершиться чи стануть доступними, існують більш ефективні способи цього досягти, наприклад, за допомогою використання join () дочекатися завершення іншого потоку, використовуючи механізм wait / notify, щоб дозволити один потік сигналізувати іншому про те, що завдання є завершеним або в ідеалі за допомогою однієї з конструкцій одночасності Java 5, таких як Semaphore або блокування черги .

18
"залишився квантовий", "весь квант" - десь по дорозі хтось забув, що означає слово "квантовий"
kbolino

@kbolino Quantum - новий атом.
Євгеній Сергєєв

2
@kbolino - ... латинська: "скільки", "скільки" . Я не бачу, як це якимось чином суперечить використанню вище. Слово просто означає описувану кількість чогось, тому поділ його на використані та залишилися частини мені здається абсолютно розумним.
Periata Breatta

@PeriataBreatta Я думаю, це має більше сенсу, якщо ви знайомі зі словом поза фізикою. Визначення фізики було єдиним, кого я знав.
kbolino

Я поставив щедрості за це питання, щоб оновити цю відповідь на 7, 8, 9. Відредагуйте її, використовуючи поточну інформацію про 7,8 і 8, і ви отримаєте щедрість.

40

Я бачу, що питання було відновлено щедро, тепер я задаю питання, для чого практичні користі yield. Наведу приклад зі свого досвіду.

Як ми знаємо, yieldзмушує викличний потік відмовитися від процесора, на якому він працює, щоб можна було запланувати інший потік. Це корисно, коли поточна нитка вже закінчила свою роботу, але хоче швидко повернутися до передньої частини черги і перевірити, чи змінилася якась умова. Чим це відрізняється від змінної умови? yieldдозволяє потоку повернутися набагато швидше до стану роботи. Під час очікування змінної умови потік призупиняється і потрібно чекати, коли інший потік подасть сигнал, що він повинен продовжуватися.yieldв основному каже: "дозвольте запустити іншу нитку, але дозвольте мені повернутися до роботи дуже скоро, оскільки я очікую, що щось дуже зміниться в моєму стані". Це натякає на зайняте спінінг, коли умова може швидко змінюватися, але призупинення потоку спричинить велику ефективність.

Але достатньо лепетання, ось конкретний приклад: паралельний малюнок хвилі фронту. Основний екземпляр цієї проблеми - обчислення окремих "островів" 1s у двовимірному масиві, заповненому 0s та 1s. "Острів" - це група комірок, які розташовані поруч з будь-яким вертикальним чи горизонтальним:

1 0 0 0
1 1 0 0
0 0 0 1
0 0 1 1
0 0 1 1

Тут ми маємо два острови 1-ї: лівий верхній і нижній правий.

Просте рішення - зробити перший прохід по всьому масиву та замінити 1 значення на приріст лічильника таким чином, щоб до кінця кожен 1 був замінений на його порядковий номер у основному порядку рядків:

1 0 0 0
2 3 0 0
0 0 0 4
0 0 5 6
0 0 7 8

На наступному кроці кожне значення замінюється мінімумом між собою та значеннями його сусідів:

1 0 0 0
1 1 0 0
0 0 0 4
0 0 4 4
0 0 4 4

Тепер ми можемо легко визначити, що у нас є два острови.

Частина, яку ми хочемо виконати паралельно, - це крок, на якому ми обчислюємо мінімум. Не вдаючись до занадто багато деталей, кожен потік отримує рядки перемежованим чином і покладається на значення, обчислені ниткою, що обробляє рядок вище. Таким чином, кожна нитка повинна трохи відставати від потоку, що обробляє попередній рядок, але також повинна йти в розумний час. Більш детально та реалізацію я сам подаю в цьому документі . Зверніть увагу на використання , sleep(0)яке більш-менш З еквівалентом yield.

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

Як бачимо, yieldдосить тонка оптимізація зерна. Використання його в неправильному місці, наприклад, очікування за умови, яка рідко змінюється, призведе до надмірного використання ЦП.

Вибачте за довгу лайку, сподіваюся, що я дав зрозуміти.


1
IUC, що ви представляєте в документі, ідея полягає в тому, що в цьому випадку ефективніше зайняти-чекати, викликаючи, yieldколи умова не задоволена, дати іншим потокам шанс продовжити обчислення, а не використовувати більш високі, Примітивні рівні синхронізації, правда?
Петро Пудлак

3
@Petr Pudlák: Так. Я порівняв це порівняно з використанням потокової сигналізації, і різниця в продуктивності в цьому випадку була величезною. Оскільки умова може стати справжньою справжністю (це ключова проблема), змінні умови занадто повільні, оскільки нитка затримується ОС, а не відмовляється від процесора на дуже короткий час використання yield.
Тюдор

@Tudor чудове пояснення!
розробник Marius Žilėnas

1
"Зверніть увагу на використання сну (0), що більше або менше еквіваленту C." .. ну, якщо ви хочете спати (0) з java, чому б ви не просто використали це? Thread.sleep () - це річ, яка вже існує. Я не впевнений, чи ця відповідь дає міркування, чому можна використовувати Thread.yield () замість Thread.sleep (0); Існує також існуюча нитка, яка пояснює, чому вони різні.
eis

@eis: Thread.sleep (0) проти Thread.yield () виходить за рамки цієї відповіді. Я лише згадував Thread.sleep (0) для людей, які шукають близький еквівалент у C. Питання стосувалося використання Thread.yield ().
Тюдор

12

Про відмінності між yield(), interrupt()і join()взагалі, не лише на Java:

  1. врожайність : Буквально «врожайність» означає відпустити, здатися, здатися. Похідний потік повідомляє операційній системі (або віртуальній машині, чи що ні), що він готовий дозволити іншим потокам запланувати її замість. Це свідчить про те, що він не робить щось занадто критичне. Це лише натяк, але не гарантований жодного ефекту.
  2. приєднання : Коли декілька потоків 'приєднуються' на якійсь ручці, токені чи об'єкті, всі вони чекають, поки всі інші відповідні потоки завершать виконання (повністю або до їх відповідного з'єднання). Це означає, що купа ниток усі виконали свої завдання. Тоді кожен з цих потоків може бути запланований для продовження іншої роботи, маючи на увазі, що всі ці завдання справді завершені. (Не плутати з SQL Joins!)
  3. переривання : Використовується одним потоком, щоб 'просунути' інший потік, який спить, або чекає, або приєднується - так, що планується продовжити його повтор, можливо, із вказівкою, що він був перерваний. (Не плутати з апаратними перервами!)

Спеціально щодо Java див

  1. Приєднання:

    Як користуватися Thread.join? (тут на StackOverflow)

    Коли приєднатись до теми?

  2. Вихід:

  3. Переривання:

    Хіба Thread.interrupt () злий? (тут на StackOverflow)


Що ви маєте на увазі, приєднавшись до ручки чи жетону? Методи wait () та notify () є на Object, що дозволяє користувачеві чекати будь-якого довільного Об'єкта. Але приєднання () здається менш абстрактним і його потрібно зателефонувати на певну тему, яку ви хочете закінчити, перш ніж продовжувати ... чи не так?
spaaarky21

@ spaaarky21: Я мав на увазі загалом, не обов'язково на Java. Крім того, a wait()- це не з'єднання, це замок на об'єкті, який намагається придбати викликовий потік - він чекає, поки замок буде звільнений іншими людьми і не буде придбаний потоком. Відповідно налаштував мою відповідь.
einpoklum

10

По-перше, власне опис

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

Тепер дуже ймовірно, що ваш основний потік виконає цикл п'ять разів до того, runяк буде виконано метод нового потоку, тому всі виклики yieldвідбуватимуться лише після того, як буде виконано цикл у основній нитці.

joinзупинить поточний потік, поки потік, що викликається, не join()буде виконано.

interruptперерве потік, на який він викликається, спричинивши InterruptException .

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


+1. Також зауважте, що після виклику return (), досі немає гарантії, що той самий потік не буде обраний для виконання знову, надаючи пул рівних пріоритетних потоків.
Ендрю Філден

Однак SwitchToThread()дзвінок краще, ніж сон (0), і це має бути помилка на Java :)
Петър Петров

4

Поточні відповіді застаріли і потребують перегляду з урахуванням останніх змін.

Немає практичної різниці Thread.yield()між версіями Java з 6 по 9.

TL; DR;

Висновки на основі вихідного коду OpenJDK ( http://hg.openjdk.java.net/ ).

Якщо не брати до уваги підтримку HotSpot зондів USDT (інформація про відстеження системи описана в посібнику dtrace ) та властивості JVM, ConvertYieldToSleepто вихідний код yield()майже однаковий. Дивіться пояснення нижче.

Java 9 :

Thread.yield()викликає специфічний для ОС метод os::naked_yield():
У Linux:

void os::naked_yield() {
    sched_yield();
}

У Windows:

void os::naked_yield() {
    SwitchToThread();
}

Java 8 і новіші версії:

Thread.yield()викликає специфічний для ОС метод os::yield():
У Linux:

void os::yield() {
    sched_yield();
}

У Windows:

void os::yield() {  os::NakedYield(); }

Як бачите, Thread.yeald()на Linux однаковий для всіх версій Java.
Давайте подивимось Windows os::NakedYield()з JDK 8:

os::YieldResult os::NakedYield() {
    // Use either SwitchToThread() or Sleep(0)
    // Consider passing back the return value from SwitchToThread().
    if (os::Kernel32Dll::SwitchToThreadAvailable()) {
        return SwitchToThread() ? os::YIELD_SWITCHED : os::YIELD_NONEREADY ;
    } else {
        Sleep(0);
    }
    return os::YIELD_UNKNOWN ;
}

Різниця між Java 9 та Java 8 у додатковій перевірці існування методу API Win32 SwitchToThread(). Той самий код є в Java 6.
Вихідний код os::NakedYield()JDK 7 дещо інший, але він має таку саму поведінку:

    os::YieldResult os::NakedYield() {
    // Use either SwitchToThread() or Sleep(0)
    // Consider passing back the return value from SwitchToThread().
    // We use GetProcAddress() as ancient Win9X versions of windows doen't support SwitchToThread.
    // In that case we revert to Sleep(0).
    static volatile STTSignature stt = (STTSignature) 1 ;

    if (stt == ((STTSignature) 1)) {
        stt = (STTSignature) ::GetProcAddress (LoadLibrary ("Kernel32.dll"), "SwitchToThread") ;
        // It's OK if threads race during initialization as the operation above is idempotent.
    }
    if (stt != NULL) {
        return (*stt)() ? os::YIELD_SWITCHED : os::YIELD_NONEREADY ;
    } else {
        Sleep (0) ;
    }
    return os::YIELD_UNKNOWN ;
}

Додаткова перевірка була скасована завдяки SwitchToThread()методу, який доступний з Windows XP та Windows Server 2003 (див. Примітки msdn ).


2

Насправді основні напрями використання урожаю ()?

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

Я вважаю, що наведений нижче код приводить до того ж результату як при використанні yield (), так і при його використанні. Це правильно?

НІ, вони дадуть різні результати. Без виходу (), як тільки нитка отримає контроль, вона виконає цикл 'Inside run' за один раз. Однак, з виходом (), як тільки нитка отримає контроль, вона надрукує "Внутрішній запуск" один раз, а потім передасть управління іншому потоку, якщо такий є. Якщо в потоці немає жодної нитки, ця нитка буде відновлена ​​знову. Тож кожен раз, коли виконується "Внутрішній запуск", він буде шукати інші потоки для виконання, і якщо жодна нитка недоступна, поточний потік буде продовжувати виконувати.

Чим урожайність () відрізняється від методів join () та interrupt ()?

return () - це надання місця іншим важливим потокам, join () - це очікування, коли інший потік завершить його виконання, а interrupt () - для переривання поточного виконавчого потоку, щоб зробити щось інше.


Просто хотів підтвердити, чи справдиться це твердження Without a yield(), once the thread gets control it will execute the 'Inside run' loop in one go? Поясніть будь ласка.
Абдулла хан

0

Thread.yield()призводить до того, що нитка переходить із стану "Запуск" у стан "Виконання". Примітка. Це не призводить до того, що нитка переходить у стан "Очікування".


@PJMeisch, RUNNINGдля java.lang.Threadекземплярів немає стану . Але це не виключає нативного "запущеного" стану для нативного потоку, для якого Threadекземпляр є проксі.
Соломон повільно

-1

Thread.yield ()

Коли ми викликаємо метод Thread.yield (), планувальник потоків утримує поточний поточний потік у стані Runnable і вибирає інший потік з рівним пріоритетом або вищим пріоритетом. Якщо немає рівної і вищої нитки пріоритету, вона переносить виклик потоку return (). Пам’ятайте, що метод виходу не змушує нитку перейти до стану «Зачекати» або «Заблоковано». Він може створити лише потік із запущеного стану до стану, що працює.

join ()

Коли виклик приєднання викликається екземпляром потоку, цей потік скаже поточному виконуваному потоку чекати, поки завершається потік приєднання. Приєднання використовується в тих ситуаціях, коли завдання, яке слід виконати до завершення поточного завдання.


-4

return () Основне використання - це перестати програму з декількома нитками на затримку.

всі ці методи відрізняються в доступі () ставить потік на затримку під час виконання іншого потоку та повертається назад після завершення цього потоку, join () приведе початок потоків разом до кінця, а інший потік запуститься після того, як цей потік має закінчилося, переривання () зупинить виконання потоку на деякий час.


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