Чи безпечний потік ExecutorService (зокрема ThreadPoolExecutor)?


77

Чи ExecutorServiceгарантує безпеку різьблення?

Я буду надсилати завдання з різних потоків до одного і того ж ThreadPoolExecutor, чи потрібно синхронізувати доступ до виконавця перед взаємодією / надсиланням завдань?

Відповіді:


31

Це правда, схожі класи JDK, здається, не дають явної гарантії безпечного подання завдань. Однак на практиці всі реалізації ExecutorService в бібліотеці справді безпечні для потоку таким чином. Я думаю, що розумно залежати від цього. Оскільки весь код, що реалізує ці функції, був розміщений у відкритому доступі, немає абсолютно жодної мотивації для того, щоб повністю переписати його по-іншому.


"розміщені у відкритому доступі" насправді? Я думав, що використовує GPL.
Raedwald

1
JDK це робить, а Дуг Лі - ні.
Кевін Бурріллон,

7
Існує достатня гарантія щодо безпечного подання завдань: див. Нижню частину javadoc interface ExecutorService, якого ThreadPoolExecutorтакож слід дотримуватися. (Більш детально в моїй нещодавно оновленій відповіді.)
Люк Ашервуд,

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

Це справедливо для будь-якого виконавця розміру.
ммм

56

( В відміну від інших відповідей) договір потокобезпечна буде документований: погляд у interfaceJavadocs (на відміну від Javadoc методів). Наприклад, внизу javadoc ExecutorService ви знайдете:

Ефекти узгодженості пам’яті: дії в потоці до подання виконуваного чи викличного завдання до ExecutorService відбуваються - до будь - яких дій, виконаних цим завданням, які, в свою чергу, відбуваються - до отримання результату через Future.get ().

Цього достатньо, щоб відповісти на це:

"чи потрібно синхронізувати доступ до виконавця перед взаємодією / поданням завдань?"

Ні, ні. Чудово конструювати та подавати завдання будь-якому (правильно реалізованому) ExecutorServiceбез зовнішньої синхронізації. Це одна з головних цілей проектування.

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

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

Подібним чином, коли ви повертаєте результат завдання назад Future.get(), отриманий потік гарантовано бачитиме всі ефекти, зроблені робочим потоком виконавця (в обох повернутих результатах, а також будь-які зміни побічних ефектів, зроблені робочим потоком) .

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

"Чи гарантує ExecutorService безпеку ниток?"

Зараз ця частина питання набагато більш загальна. Наприклад, не вдалося знайти жодного твердження договору про безпеку потоків щодо методу shutdownAndAwaitTermination- хоча я зауважую, що зразок коду в Javadoc не використовує синхронізацію. (Хоча, можливо, існує приховане припущення, що зупинку ініціює той самий потік, який створив Виконавець, а не, наприклад, робочий потік?)

До речі, я рекомендую книгу "Java Concurrency In Practice" для кращого ознайомлення зі світом паралельного програмування.


3
Це не повна гарантія безпеки ниток; вони встановлюють порядок видимості лише за певних обставин. Наприклад, немає чітко задокументованої гарантії того, що безпечно викликати execute () із декількох потоків (поза контекстом завдань, що виконуються на виконавці).
Майлз

1
@Miles Через кілька років більше досвіду :-) ... я не згоден. Взаємозв'язок "відбувається раніше" є фундаментальним поняттям у (новаторській) Моделі пам'яті Java, представленій у Java 5, яка, в свою чергу, утворює основний будівельний блок для визначення одночасних (на відміну від синхронізованих) контрактів на безпеку потоків. (І я знову підтримую свою оригінальну відповідь, хоча, сподіваюся, тепер це зрозуміліше з деяким редагуванням.)
Люк Ашервуд,

9

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

Якщо представлені Runnable/ Callableпосилання сумісництвом структури даних , яка доступна з інших Runnable/ Callableсек випадків (потенційно оброблюваних simulataneously різних потоків), то це ваша відповідальність , щоб забезпечити безпеку потоку через цю структуру даних.

Щоб відповісти на другу частину вашого запитання, так, перед тим, як подавати будь-які завдання, ви матимете доступ до ThreadPoolExeecuter; напр

BlockingQueue<Runnable> workQ = new LinkedBlockingQueue<Runnable>();
ExecutorService execService = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.SECONDS, workQ);
...
execService.submit(new Callable(...));

РЕДАГУВАТИ

На основі коментаря Брайана та на випадок, якщо я неправильно зрозумів ваше запитання: Подання завдань із декількох виробницьких потоків до ExecutorService, як правило, є безпечним для потоків (незважаючи на те, що явно не згадується в інтерфейсі API, наскільки я можу зрозуміти). Будь-яка реалізація, яка не забезпечує безпеку потоків, була б марною в багатопотоковому середовищі (оскільки багато виробників / декілька споживачів є досить поширеною парадигмою), і саме для цього ExecutorService(і для решти java.util.concurrent) було розроблено.


8
Хіба не те, що він запитує, полягає в тому, що подання є безпечним для потоків? тобто що він може подавати з різних тем
Брайан Егнью

1
Так, я запитую, чи безпечно надсилати завдання в один і той же екземпляр ThreadPoolExecutor з декількох потоків. Оновлено питання, оскільки важливе слово "синхронізувати" зникло: |
leeeroy

1
"Будь-яка реалізація, яка не забезпечує безпеку потоків, буде марною в багатопотоковому середовищі": для гіпотетичного ExecutorService не зовсім неправдоподібно забезпечити безпечну реалізацію без потоків, оскільки один виробник є досить поширеною моделлю. (Але для ThreadPoolExecutor, призначеного для загального користування, цей коментар, безумовно, є)
Майлз

6

Бо ThreadPoolExecutorвідповідь просто так . ExecutorServiceніяк НЕ санкціонувати або іншим чином гарантувати , що всі реалізації потокобезпечна, і вона не може , як це інтерфейс. Ці типи контрактів виходять за рамки інтерфейсу Java. Однак і те, ThreadPoolExecutorі інше чітко задокументовано як безпечне для потоків. Більше того, ThreadPoolExecutorкерує своєю чергою завдань, використовуючи java.util.concurrent.BlockingQueueінтерфейс, який вимагає, щоб усі реалізації були потокобезпечними. Будь-яку java.util.concurrent.*реалізацію BlockingQueueможна сміливо вважати безпечною для потоків. Будь-яка нестандартна реалізація може цього не робити, хоча це було б абсолютно безглуздо, якби хтось запропонував BlockingQueueчергу впровадження, яка не була безпечною для потоків.

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


4
Інтерфейси можуть і дозволяють реалізовувати поточно безпечні реалізації. Безпека ниток - це документований контракт, як і будь-який інший тип поведінки (наприклад List.hashCode()). У Java-документах сказано, що "реалізації BlockingQueue безпечні для потоку" (отже, не безпечна для потоків BlockingQueue є не просто безглуздою, а помилковою), але такої документації для ThreadPoolExecutor або будь-якого інтерфейсу, який він реалізує, немає.
Майлз

1
Чи можете ви ознайомитись із документацією, в якій чітко зазначено, що ThreadPoolExecutorвона захищена від потоків?
mapeters

2

Для ThreadPoolExecutor його подання є потокобезпечним. Ви можете побачити вихідний код у jdk8. Додаючи нове завдання, він використовує mainLock, щоб забезпечити безпеку потоку.

private boolean addWorker(Runnable firstTask, boolean core) {
            retry:
            for (;;) {
                int c = ctl.get();
                int rs = runStateOf(c);

                // Check if queue empty only if necessary.
                if (rs >= SHUTDOWN &&
                    ! (rs == SHUTDOWN &&
                       firstTask == null &&
                       ! workQueue.isEmpty()))
                    return false;

                for (;;) {
                    int wc = workerCountOf(c);
                    if (wc >= CAPACITY ||
                        wc >= (core ? corePoolSize : maximumPoolSize))
                        return false;
                    if (compareAndIncrementWorkerCount(c))
                        break retry;
                    c = ctl.get();  // Re-read ctl
                    if (runStateOf(c) != rs)
                        continue retry;
                    // else CAS failed due to workerCount change; retry inner loop
                }
            }

            boolean workerStarted = false;
            boolean workerAdded = false;
            Worker w = null;
            try {
                w = new Worker(firstTask);
                final Thread t = w.thread;
                if (t != null) {
                    final ReentrantLock mainLock = this.mainLock;
                    mainLock.lock();
                    try {
                        // Recheck while holding lock.
                        // Back out on ThreadFactory failure or if
                        // shut down before lock acquired.
                        int rs = runStateOf(ctl.get());

                        if (rs < SHUTDOWN ||
                            (rs == SHUTDOWN && firstTask == null)) {
                            if (t.isAlive()) // precheck that t is startable
                                throw new IllegalThreadStateException();
                            workers.add(w);
                            int s = workers.size();
                            if (s > largestPoolSize)
                                largestPoolSize = s;
                            workerAdded = true;
                        }
                    } finally {
                        mainLock.unlock();
                    }
                    if (workerAdded) {
                        t.start();
                        workerStarted = true;
                    }
                }
            } finally {
                if (! workerStarted)
                    addWorkerFailed(w);
            }
            return workerStarted;
        }

1

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

Так, вказано взаємозв'язок " до-до" , але це не означає нічого про безпеку потоків самих методів, як прокоментував Майлз . У відповіді Люка Ашервуда зазначено, що першого достатньо, щоб довести друге, але фактичних аргументів не наводиться.

"Безпека потоків" може означати різні речі, але ось простий контра-приклад Executor(не, ExecutorServiceале це не має значення), який тривіально відповідає необхідним відносинам " до-до", але не є безпечним для потоків через несинхронізований доступ до countполя .

class CountingDirectExecutor implements Executor {

    private int count = 0;

    public int getExecutedTaskCount() {
        return count;
    }

    public void execute(Runnable command) {
        command.run();
    }
}

Застереження: Я не фахівець, і я знайшов це запитання, оскільки сам шукав відповідь.


Все, що ви заявляєте, є правдою, але питання конкретно задає питання "чи повинен я синхронізувати доступ до виконавця" - тому я читав "безпеку потоків" у цьому контексті, щоб говорити лише про безпеку потоків (стан / дані всередині ) виконавець, та дії з використанням його методів.
Люк Ашервуд,

Як зробити так, щоб подані завдання мали "побічні ефекти, безпечні для потоків" - це набагато більша тема! (Це набагато простіше навколо, якщо вони цього не роблять. Мовляв, якщо якийсь незмінний обчислюваний результат можна просто повернути назад. Коли вони торкнуться змінного спільного стану, то впевнені: вам потрібно подбати про те, щоб визначити і зрозуміти межі потоків і врахувати захист від ниток, глухі замки, живі замки тощо)
Люк Ашервуд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.