Як рамка fork / join краща, ніж пул потоків?


134

Які переваги використання нової системи fork / join в простому розділенні на початку великого завдання на N підзадач, відправці їх у кешований пул потоків (від виконавців ) та очікуванні завершення кожного завдання? Я не бачу, як використання абстракції / приєднання об'єднання спрощує проблему або робить рішення більш ефективним у порівнянні з тим, що було у нас уже роками.

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

public class Blur implements Runnable {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;

    private int mBlurWidth = 15; // Processing window size, should be odd.

    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    public void run() {
        computeDirectly();
    }

    protected void computeDirectly() {
        // As in the example, omitted for brevity
    }
}

На початку розділіть і надішліть завдання до пулу потоків:

// source image pixels are in src
// destination image pixels are in dst
// threadPool is a (cached) thread pool

int maxSize = 100000; // analogous to F-J's "sThreshold"
List<Future> futures = new ArrayList<Future>();

// Send stuff to thread pool:
for (int i = 0; i < src.length; i+= maxSize) {
    int size = Math.min(maxSize, src.length - i);
    ForkBlur task = new ForkBlur(src, i, size, dst);
    Future f = threadPool.submit(task);
    futures.add(f);
}

// Wait for all sent tasks to complete:
for (Future future : futures) {
    future.get();
}

// Done!

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

Я щось пропускаю? Яка додаткова цінність використання системи fork / join?

Відповіді:


136

Я думаю, що основне непорозуміння полягає в тому, що приклади Fork / Join не показують крадіжку роботи, а лише якісь стандартні ділення та перемоги.

Крадіжка роботи була б такою: працівник Б закінчив свою роботу. Він такий добрий, тож він озирається і бачить Worker A як і раніше дуже важко працює. Він прогулюється і запитує: "Ей, хлопче, я міг би тобі допомогти". Відповіді. "Класно, у мене це завдання 1000 одиниць. Поки я закінчив 345, покинувши 655. Чи можете ви, будь ласка, працювати над номером 673 до 1000, я зроблю 346 до 672." B каже: "Добре, почнемо, щоб ми могли піти в паб раніше".

Розумієте - працівники повинні спілкуватися між собою, навіть коли вони розпочали справжню роботу. Це відсутність у прикладах.

З іншого боку, приклади показують лише щось на зразок "використовувати субпідрядників":

Працівник A: "Данг, у мене 1000 одиниць роботи. Занадто багато для мене. Я сам зроблю 500 і підряд підписав 500 для когось іншого". Це триває, поки велике завдання не буде розбито на невеликі пакети по 10 одиниць кожен. Вони будуть виконані наявними працівниками. Але якщо один пакетик є своєрідною отруйною таблеткою і займає значно довше, ніж інші пакетики - невдача, фаза поділу закінчена.

Єдина відмінна різниця між Fork / Join та розділенням завдання вперед полягає в наступному: Під час поділу наперед ви маєте робочу чергу з самого початку. Приклад: 1000 одиниць, поріг - 10, тому в черзі є 100 записів. Ці пакети розподіляються між членами нитки.

Fork / Join є складнішим і намагається зменшити кількість пакетів у черзі меншою:

  • Крок 1: Поставте один пакет, що містить (1 ... 1000) у чергу
  • Крок 2: Один працівник вискакує пакет (1 ... 1000) і замінює його двома пакетами: (1 ... 500) і (501 ... 1000).
  • Крок 3: Один працівник спливає пакет (500 ... 1000) і штовхає (500 ... 750) і (751 ... 1000).
  • Крок n: Стек містить ці пакети: (1..500), (500 ... 750), (750 ... 875) ... (991..1000)
  • Крок n + 1: Пакет (991..1000) спливає і виконується
  • Крок n + 2: Пакет (981..990) спливає і виконується
  • Крок n + 3: Пакет (961..980) спливає і розбивається на (961 ... 970) і (971..980). ….

Ви бачите: у Fork / Join черга менша (6 у прикладі), і фази "розділення" та "робота" переплітаються.

Коли декілька робітників вискакують і штовхають одночасно, взаємодії, звичайно, не так зрозумілі.


Я думаю, що це справді відповідь. Цікаво, чи є де-небудь фактичні приклади Fork / Join, які також демонстрували б його крадіжки? З елементарними прикладами обсяг робочого навантаження досить ідеально передбачуваний за розміром одиниці (наприклад, довжина масиву), тому попереднє розділення легко. Викрадення, безумовно, призведе до різниці в проблемах, коли обсяг навантаження на одиницю недостатньо передбачуваний від розміру одиниці.
Joonas Pulakaka

AH Якщо ваша відповідь правильна, це не пояснює як. Наведений Oracle приклад не призводить до крадіжки роботи. Як би роздвоюватися та приєднатися до роботи, як у прикладі, який ви описуєте тут? Чи можете ви показати якийсь код Java, який би зробив форк та приєднався до крадіжки роботи так, як ви описуєте її? дякую
Марк

@Marc: Вибачте, але у мене немає прикладу.
AH

6
Проблема прикладу Oracle, IMO, полягає не в тому, що він не демонструє крадіжку роботи (це робиться, як описано в AH), а в тому, що легко кодувати алгоритм для простого ThreadPool, як і (як це робив Joonas). FJ є найбільш корисним, коли робота не може бути попередньо розділена на достатньо незалежні завдання, але рекурсивно може бути розділена на завдання, незалежні між собою. Дивіться мою відповідь для прикладу
ashirley

2
Деякі приклади, коли крадіжка може стати корисною: h-online.com/developer/features/…
залп

27

Якщо у вас є n зайнятих ниток, які працюють на 100% незалежно, це буде краще, ніж п потоків у пулі Fork-Join (FJ). Але це ніколи не виходить таким чином.

Можливо, не вдасться точно розділити проблему на n рівних частин. Навіть якщо ви це робите, планування ниток - це певний спосіб бути справедливим. Ви закінчите чекати найповільнішої нитки. Якщо у вас є кілька завдань, то вони можуть виконуватись з меншим, ніж n-бічним паралелізмом (як правило, більш ефективним), але все ж перейти на n-шлях, коли інші завдання закінчені.

То чому б ми просто не вирішили проблему на шматки розміру FJ і не працювали над цим пулом. Типове використання FJ вирішує проблему на крихітні шматочки. Робити це у випадковому порядку потрібно багато координації на апаратному рівні. Накладні витрати були б вбивцями. У FJ завдання ставляться на чергу, яку нитка зчитує в порядку "Останній в першому виході" (LIFO / стек), а крадіжка роботи (в основному, як правило) виконується "Перше в першому виході" (FIFO / "черга"). Результат полягає в тому, що обробку довгих масивів можна проводити значною мірою послідовно, навіть якщо вона розбита на крихітні шматки. (Окрім того, може не тривіально розбити проблему на невеликі шматки рівномірного розміру за один великий удар. Скажіть, що маєте справу з якоюсь формою ієрархії, не врівноважуючи її.)

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


Але чому б FJ в кінцевому підсумку не чекав і найповільнішої нитки? Існує заздалегідь кількість підзадач, і, звичайно, деякі з них завжди будуть останніми. Коригування maxSizeпараметра в моєму прикладі призведе до майже подібного поділу підзадач, як "бінарне розщеплення" у прикладі FJ (зроблено в рамках compute()методу, який або обчислює щось, або надсилає підзадачі invokeAll()).
Joonas Pulakaka

Тому що їх набагато менше - я додам до своєї відповіді.
Том Хотін - тайклін

Гаразд, якщо кількість підзадач на порядок більше (-их), ніж можна реально паралельно обробляти (що має сенс, щоб уникнути необхідності чекати останнього), то я можу побачити проблеми координації. Приклад FJ може бути оманливим, якщо поділ повинен бути таким детальним: він використовує поріг 100000, який для зображення 1000x1000 створює 16 фактичних підзадач, кожне обробляє 62500 елементів. Для зображення розміром 10000x10000 було б 1024 підзадачі, а це вже щось.
Joonas Pulakaka

19

Кінцева мета потокових пулів і Fork / Join однакова: обидва хочуть використовувати наявну потужність процесора якнайкраще, щоб досягти максимальної пропускної здатності. Максимальна пропускна здатність означає, що якомога більше завдань має бути виконано протягом тривалого періоду часу. Що для цього потрібно? (Для наступного ми припустимо, що задач на обчислення не бракує. Завжди достатньо зробити для 100% використання процесора. Додатково я використовую "CPU" рівномірно для ядер або віртуальних ядер у разі гіперточки).

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

Таким чином ми з'ясували, що для максимальної пропускної здатності нам потрібно мати точно таку ж кількість потоків, ніж процесори. У розмитому прикладі Oracle ви можете взяти пул потоків фіксованого розміру з кількістю потоків, рівним кількості доступних процесорів, або використовувати пул потоків. Це не змінить значення, ви праві!

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

class AbcAlgorithm implements Runnable {
    public void run() {
        Future<StepAResult> aFuture = threadPool.submit(new ATask());
        StepBResult bResult = stepB();
        StepAResult aResult = aFuture.get();
        stepC(aResult, bResult);
    }
}

Тут ми бачимо алгоритм, який складається з трьох етапів A, B і C. нитковий пул і виконайте завдання b безпосередньо. Після цього потік буде чекати виконання завдання А також, і продовжить з кроком С. Якщо A і B виконані одночасно, то все добре. Але що робити, якщо A займає більше часу, ніж B? Це може бути тому, що диктує характер завдання A, але це може бути і так, тому що немає завдання для завдання A, доступного на початку, і завдання A потрібно почекати. (Якщо доступний лише один процесор, і, отже, у вашій нитці є лише один потік, це навіть спричинить тупик, але поки що це, крім точки). Справа в тому, що нитка, яка щойно виконала завдання Bблокує цілу нитку . Оскільки у нас така ж кількість потоків, що і в процесорі, і один потік заблокований, це означає, що один процесор простоює .

Fork / Join вирішує цю проблему: у рамках fork / join ви записуєте той самий алгоритм, як наступний:

class AbcAlgorithm implements Runnable {
    public void run() {
        ATask aTask = new ATask());
        aTask.fork();
        StepBResult bResult = stepB();
        StepAResult aResult = aTask.join();
        stepC(aResult, bResult);
    }
}

Виглядає так само, чи не так? Однак підказка в тому, що aTask.join не буде блокувати . Натомість тут грає робота-крадіжка : Нитка шукатиме навколо себе інші завдання, які були роздвоєні в минулому, і продовжуватиметься з ними. Спочатку він перевіряє, чи почали опрацьовувати задачі, які він сам розв'язав. Отже, якщо A ще не був запущений іншим потоком, він зробить A next, інакше він перевірить чергу інших потоків і вкраде їх роботу. Після завершення цього завдання іншого потоку він перевірить, чи завершено A зараз. Якщо це описаний вище алгоритм, можна викликати stepC. Інакше буде шукати ще одне завдання вкрасти. Таким чином, пули fork / join можуть досягти 100% використання процесора навіть за умови блокування .

Однак є пастка: Робота з крадіжкою можлива лише для joinвиклику ForkJoinTasks. Це не можна зробити для зовнішніх блокуючих дій, таких як очікування іншого потоку або очікування дії вводу / виводу. То що з цього приводу, чекати завершення вводу / виводу є загальним завданням? У цьому випадку, якщо ми зможемо додати додатковий потік до пулу Fork / Join, який буде знову зупинено, як тільки дія блокування завершиться, буде другою найкращою справою. І ForkJoinPoolнасправді можна зробити саме це, якщо ми використовуємо ManagedBlockers.

Фібоначчі

У JavaDoc for RecursiveTask є прикладом для обчислення чисел Фібоначчі за допомогою Fork / Join. Класичне рекурсивне рішення див .:

public static int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

Як пояснено в JavaDocs, це досить демпфіруючий спосіб обчислення чисел чисел, оскільки цей алгоритм має складність O (2 ^ n), тоді як можливі більш прості способи. Однак цей алгоритм дуже простий і зрозумілий, тому ми його дотримуємося. Припустимо, ми хочемо прискорити це за допомогою Fork / Join. Наївна реалізація виглядала б так:

class Fibonacci extends RecursiveTask<Long> {
    private final long n;

    Fibonacci(long n) {
        this.n = n;
    }

    public Long compute() {
        if (n <= 1) {
            return n;
        }
        Fibonacci f1 = new Fibonacci(n - 1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n - 2);
        return f2.compute() + f1.join();
   }
}

Етапи, на які розбита ця Завдання, є занадто короткими, і це призведе до жахливих дій, але ви можете бачити, як рамка взагалі працює дуже добре: обидві суми можна обчислити самостійно, але тоді нам потрібно обоє для складання остаточного результат. Так одна половина робиться в іншій нитці. Забавляйтеся робити те саме з пулами ниток, не отримуючи тупик (можливо, але не майже так просто).

Просто для повноти: Якщо ви насправді хочете обчислити числа Фібоначчі, використовуючи цей рекурсивний підхід, ось оптимізована версія:

class FibonacciBigSubtasks extends RecursiveTask<Long> {
    private final long n;

    FibonacciBigSubtasks(long n) {
        this.n = n;
    }

    public Long compute() {
        return fib(n);
    }

    private long fib(long n) {
        if (n <= 1) {
            return 1;
        }
        if (n > 10 && getSurplusQueuedTaskCount() < 2) {
            final FibonacciBigSubtasks f1 = new FibonacciBigSubtasks(n - 1);
            final FibonacciBigSubtasks f2 = new FibonacciBigSubtasks(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        } else {
            return fib(n - 1) + fib(n - 2);
        }
    }
}

Це робить підзадачі набагато меншими, оскільки вони розбиті лише тоді, коли n > 10 && getSurplusQueuedTaskCount() < 2це правда, це означає, що існує значно більше 100 методів викликів do ( n > 10) і вже не дуже важливі завдання людини ( getSurplusQueuedTaskCount() < 2).

На моєму комп’ютері (4 ядра (8 при підрахунку Hyper-Threading), Intel (R) Core (TM) i7-2720QM CPU @ 2,20 ГГц) fib(50)займає 64 секунди при класичному підході і всього 18 секунд при підході Fork / Join, який це досить помітний виграш, хоча не настільки теоретично, наскільки це можливо.

Підсумок

  • Так, у вашому прикладі Fork / Join не має переваги перед класичними пулами потоків.
  • Форк / приєднання може різко підвищити продуктивність, коли входить блокування
  • Fork / Join обходить деякі проблеми з тупиком

17

Fork / join відрізняється від пулу потоків тим, що він реалізує роботу крадіжки. З вилки / приєднайтесь

Як і будь-яка програма ExecutorService, структура fork / join розподіляє завдання робочим потокам в пулі потоків. Рамка fork / join є чіткою, оскільки вона використовує алгоритм робочої крадіжки. Робітники, у яких не вистачає справ, можуть вкрасти завдання з інших потоків, які все ще зайняті.

Скажімо, у вас є дві нитки та 4 завдання a, b, c, d, які займають відповідно 1, 1, 5 та 6 секунд. Спочатку a і b призначаються для потоку 1 і c і d до потоку 2. У пулі ниток це займе 11 секунд. За допомогою вилки / з'єднання нитка 1 закінчується і може вкрасти роботу з потоку 2, тож завдання d в кінцевому підсумку буде виконано потоком 1. Нитка 1 виконує a, b і d, нитка 2 просто c. Загальний час: 8 секунд, а не 11.

EDIT: Як зазначає Joonas, завдання не обов'язково попередньо виділяються для потоку. Ідея fork / join полягає в тому, що нитка може вибрати поділ задачі на кілька підрозділів. Отже, щоб перезапустити вищесказане:

У нас є два завдання (ab) та (cd), які займають відповідно 2 та 11 секунд. Нитка 1 починає виконувати ab і розділяє її на дві підзадачі a & b. Аналогічно з потоком 2 вона розбивається на дві підзавдання c & d. Коли нитка 1 закінчила & b, вона може вкрасти d з потоку 2.


5
Пулові нитки - це типи екземплярів ThreadPoolExecutor . Таким чином, завдання переходять до черги ( BlockingQueue на практиці), з якої робочі нитки приймають завдання, як тільки вони закінчили своє попереднє завдання. Наскільки я розумію, завдання не призначені заздалегідь певним потокам. Кожна нитка має (максимум) 1 завдання за раз.
Joonas Pulakaka

4
AFAIK є одна черга на один ThreadPoolExecutor, який, в свою чергу, управляє декількома Нитками. Це означає, що присвоєння завдань або Runnables (не ниткам!) Виконавцю завдання також не попередньо розміщені на певних потоках. Точно так це робить і FJ. Поки ніякої користі для використання FJ.
AH

1
@AH Так, але fork / join дозволяє розділити поточне завдання. Нитка, яка виконує завдання, може розділити її на дві різні задачі. Тож у ThreadPoolExecutor у вас є фіксований перелік завдань. За допомогою fork / join завдання виконання може розділити власну задачу на дві частини, які потім можуть бути вибрані іншими потоками, коли вони закінчать роботу. Або ви, якщо закінчите першим.
Меттью Фарвелл

1
@Matthew Farwell: У прикладі FJ у кожному завданні compute()або обчислюється завдання, або розбивається на два підзадачі. Який варіант він вибере, залежить лише від розміру завдання ( if (mLength < sThreshold)...), тому це просто фантазійний спосіб створення фіксованої кількості завдань. Для зображення розміром 1000x1000 буде рівно 16 підзадач, які насправді щось обчислюють. Крім того, буде 15 (= 16 - 1) "проміжних" завдань, які генерують і викликають лише підзадачі і самі нічого не обчислюють.
Joonas Pulakaka

2
@Matthew Farwell: Цілком можливо, що я не розумію всіх FJ, але якщо підзадача вирішила виконати його computeDirectly()метод, більше нічого не можна вкрасти. Весь розщеплення робиться апріорно , принаймні в прикладі.
Joonas Pulakaka

14

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

Основна перевага - ефективна координація між робочими нитками. Роботу потрібно розділити і знову зібрати, що вимагає координації. Як ви бачите у відповіді AH над кожним потоком є ​​свій робочий список. Важливою властивістю цього списку є його сортування (великі завдання вгорі та невеликі завдання внизу). Кожен потік виконує завдання в нижній частині свого списку і викрадає завдання вгорі інших списків потоків.

Результатом цього є:

  • Голова та хвіст списків завдань можуть синхронізуватися незалежно, зменшуючи суперечки у списку.
  • Значні підтрубки твору розбиваються і збираються повторно одним і тим же потоком, тому для цих підтрубок не потрібно узгоджувати міжрядкові потоки.
  • Коли нитка краде роботу, вона займає великий шматок, який потім підрозділяє на свій власний список
  • Робоча сталізація означає, що нитки майже повністю використовуються до кінця процесу.

Більшість інших схем розділення та підкорення за допомогою потокових пулів потребують більшої комунікації та координації між потоками.


13

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

Ось приємна стаття на цю тему. Цитата:

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


8

Ще одна важлива відмінність полягає в тому, що за допомогою FJ ви можете робити кілька складних фаз «Приєднання». Розглянемо вид злиття з http://facturing.ycp.edu/~dhovemey/spring2011/cs365/lecture/lecture18.html , для попереднього розбиття цієї роботи потрібно буде занадто багато оркестрацій. наприклад, Вам потрібно зробити наступні дії:

  • сортувати першу чверть
  • сортувати другу чверть
  • об'єднайте перші 2 чверті
  • сортувати третю чверть
  • сортувати четверту чверть
  • об'єднати останні 2 чверті
  • з’єднайте 2 половинки

Як вказати, що ви повинні робити сорти перед злиттями, які стосуються їх тощо.

Я розглядав, як найкраще зробити певну річ для кожного зі списку предметів. Я думаю, що я просто заздалегідь розділити список і використовувати стандартний ThreadPool. FJ видається найбільш корисним, коли робота не може бути попередньо розділена на достатньо незалежні завдання, але може бути рекурсивно розділена на завдання, незалежні між собою (наприклад, сортування половинок є незалежними, але об'єднання 2 відсортованих половинок у відсортоване ціле не є).


6

F / J також має чітку перевагу, коли у вас є дорогі операції з об'єднання. Оскільки воно розбивається на структуру дерева, ви виконуєте лише злиття log2 (n) на відміну від n злиття з лінійним розщепленням ниток. (Це робить теоретичне припущення, що у вас є стільки процесорів, скільки потоків, але все-таки перевага.) Для домашнього завдання нам довелося об'єднати декілька тисяч 2D-масивів (усіх однакових розмірів) шляхом підсумовування значень у кожному індексі. З процесорами приєднання fork та P час наближається до log2 (n), оскільки P наближається до нескінченності.

1 2 3 .. 7 3 1 .... 8 5 4
4 5 6 + 2 4 3 => 6 9 9
7 8 9 .. 1 1 0 .... 8 9 9


3

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

Логіка Fork / Join дуже проста: (1) розділити (виделкою) кожне велике завдання на менші завдання; (2) обробляти кожне завдання окремим потоком (розділяючи їх на ще менші завдання, якщо потрібно); (3) приєднайтеся до результатів.


3

Якщо проблема така, що нам доведеться чекати, коли інші потоки завершаться (як у випадку сортування масиву чи суми масиву), слід використовувати приєднання до fork, оскільки Executor (Executors.newFixedThreadPool (2)) задихнеться через обмеженість кількість ниток. Пул forkjoin створить у цьому випадку більше потоків для прикриття заблокованої нитки, щоб підтримувати той же паралелізм

Джерело: http://www.oracle.com/technetwork/articles/java/fork-join-422606.html

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

Рамка fork / join додана в пакет java.util.concurrent в Java SE 7 завдяки зусиллям Дуга Леа заповнює цей пробіл

Джерело: https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html

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

public int getPoolSize () Повертає кількість робочих ниток, які почалися, але ще не завершені. Результат, повернутий цим методом, може відрізнятися від getParallelism (), коли нитки створюються для підтримки паралелізму, коли інші спільно блокуються.


2

Я хотів би додати коротку відповідь для тих, хто не має багато часу для читання довгих відповідей. Порівняння взято з книги "Прикладні візерунки Акки":

Ваше рішення щодо того, чи використовувати fork-join-исполнитель або виконавець потокового пулу, в значній мірі ґрунтується на тому, чи будуть блоковані операції в цьому диспетчері. Виконавець fork-join - дає вам максимальну кількість активних потоків, тоді як виконавець пулу потоків дає вам фіксовану кількість потоків. Якщо потоки заблоковані, виконавець fork-join-виконавець створить більше, тоді як пул-потоків-виконавець не буде. Для блокування операцій вам, як правило, краще з виконавцем потоків пулів-виконавців, оскільки це запобігає вибуху ваших потоків. Більше "реактивних" операцій краще у вил-приєднанні-виконавця.

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