Чому створення теми, як кажуть, коштує дорого?


180

Навчальні програми Java кажуть, що створити Тему дорого. Але чому саме це дорого? Що саме відбувається, коли створена нитка Java, що робить її створення дорогим? Я вважаю твердження правдивим, але мене просто цікавить механіка створення нитки в JVM.

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

Від Java Паралелізм на практиці
Брайан Гетц, Тім Пайерлса, Джошуа Блох, Джозеф Bowbeer, Девід Холмс, Doug Lea
Друк ISBN-10: 0-321-34960-1


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

9
@typoknig - Дороге порівняно з НЕ створенням нової
теми


1
нитки для виграшу. не потрібно завжди створювати нові теми для завдань.
Олександр Міллз

Відповіді:


149

Створення ниток Java коштує дорого, тому що тут задіяно досить багато роботи:

  • Для стека ниток потрібно виділити і ініціалізувати великий блок пам'яті.
  • Для створення / реєстрації нативного потоку в хост-операції потрібно здійснити системні виклики.
  • Дескриптори повинні бути створені, ініціалізовані та додані до внутрішніх структур даних JVM.

Також дорого в тому сенсі, що нитка зав'язує ресурси, поки вона жива; наприклад, стек потоків, будь-які об'єкти, доступні зі стека, дескриптори потоку JVM, дескриптори натільних потоків ОС.

Витрати на всі ці речі залежать від платформи, але вони недешеві на будь-якій платформі Java, яку я коли-небудь стикався.


Пошук Google знайшов мені старий показник, який повідомляє про швидкість створення потоку ~ 4000 в секунду на Sun Java 1.4.1 на старовинному подвійному процесорі Xeon 2002 під управлінням 2002 vintage Linux. Більш сучасна платформа дасть кращі цифри ... і я не можу коментувати методологію ... але, принаймні, це дає змогу створити дорогу нитку.

Бенчмаркінг Пітера Лорі вказує на те, що створення потоків значно швидше в ці дні в абсолютних показниках, але незрозуміло, наскільки це пояснюється поліпшенням Java та / або ОС ... або більшою швидкістю процесора. Але його цифри все ще вказують на поліпшення в 150 разів, якщо ви використовуєте пул потоків проти створення / запуску нової нитки кожного разу. (І він зазначає, що це все відносно ...)


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


Я трохи прокопав, щоб побачити, як насправді виділяється стек потоку Java. У випадку OpenJDK 6 в Linux стек потоків розподіляється викликом, pthread_createякий створює нативний потік. (JVM не передає pthread_createпопередньо виділений стек.)

Потім всередині pthread_createстека виділяється викликом mmapнаступним чином:

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

Відповідно man mmap, MAP_ANONYMOUSпрапор викликає ініціалізацію пам'яті до нуля.

Таким чином, навіть якщо це може бути не суттєво, що нові стеки потоків Java нульові (за специфікацією JVM), на практиці (принаймні, з OpenJDK 6 в Linux) вони нульові.


2
@Raedwald - це частина ініціалізації, яка дорога. Десь щось (наприклад, GC або ОС) зведе нуль байтів, перш ніж блок перетвориться на стек потоків. Це вимагає циклів фізичної пам'яті на типовому обладнання.
Стівен С

2
"Десь щось (наприклад, GC або ОС) зведе нуль байтів". Це буде? Операційна система, якщо вимагає виділення нової сторінки пам'яті, з міркувань безпеки. Але це буде нечасто. І ОС може зберігати кеш сторінок з уже нульовими редакторами (IIRC, Linux це робить). Чому GC турбує, враховуючи, що JVM не дозволить будь-якій програмі Java читати її вміст? Зауважте, що стандартна malloc()функція C , яку JVM цілком може використовувати, не гарантує, що виділена пам’ять буде нульовою (можливо, щоб уникнути саме таких проблем з продуктивністю).
Raedwald

1
stackoverflow.com/questions/2117072/… погоджується, що "Одним з основних факторів є пам'ять стека, що виділяється кожному потоку".
Raedwald

2
@Raedwald - перегляньте оновлену відповідь для інформації про те, як насправді розподіляється стек.
Stephen C

2
Можливо (ймовірно навіть), що сторінки пам'яті, виділені mmap()викликом, копіюються під час запису, відображаються на нульовій сторінці, тому їх ініціалізація відбувається не всередині mmap()себе, а коли спочатку записуються сторінки , а потім лише одна сторінка на Час. Тобто, коли потік починає виконання, при цьому витрачається вартість на створений потік, а не на потік творця.
Raedwald

76

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

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

Ще одна альтернатива - використання пулу потоків. Пул потоків може бути більш ефективним з двох причин. 1) він повторно використовує нитки, вже створені. 2) ви можете налаштувати / контролювати кількість потоків, щоб забезпечити оптимальну продуктивність.

Наступна програма друкує ....

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

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

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

Як бачите, створення нової нитки коштує лише ~ 70 мкс. Це можна вважати тривіальним у багатьох, якщо не в більшості випадків використання. Щодо кажучи, це дорожче, ніж альтернативи, а для деяких ситуацій кращим рішенням є пул потоків або взагалі не використання ниток.


8
Це чудовий фрагмент коду. Лаконічний, до речі і чітко відображає його суть.
Микола

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

@VictorGrazi Я припускаю, що ви хочете збирати результати централізовано. Це виконує однакову кількість чергових робіт у кожному випадку. Відлік засувки був би трохи швидшим.
Пітер Лорі

Насправді, чому б просто не зробити це робити щось послідовно швидко, наприклад, збільшуючи лічильник; відкиньте всю річ BlockingQueue. Перевірте лічильник наприкінці, щоб запобігти оптимізації компілятора операції збільшення
Віктор Гразі

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

31

Теоретично це залежить від СВМ. На практиці кожен потік має відносно велику кількість пам'яті стека (думаю, 256 Кб за замовчуванням). Крім того, потоки реалізовані як потоки ОС, тому їх створення включає виклик ОС, тобто контекстний комутатор.

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


9
Btw kb = кіло-біт, kB = кілобайт. Gb = гіга-біт, GB = гігабайт.
Пітер Лорі

@PeterLawrey чи використовуємо великі літери 'k' у 'kb' та 'kB', тож є симетрія до 'Gb' та 'GB'? Ці речі мене клопочуть.
Джек

3
@Jack Є K = 1024 та k= 1000.;) en.wikipedia.org/wiki/Кібібайт
Пітер Лорі

9

Існує два види потоків:

  1. Власні нитки : це абстракції навколо ниток базових операційних систем. Тому створення ниток є таким же дорогим, як і система - завжди є накладні витрати.

  2. "Зелені" нитки : створені та заплановані JVM, вони дешевші, але належного паралелізму не виникає. Вони поводяться як потоки, але виконуються в межах потоку JVM в ОС. Наскільки мені відомо, вони не часто використовуються.

Найбільший фактор, про який я можу подумати при створенні потоку, - це розмір стека, який ви визначили для своїх потоків. Розмір стека нитки може передаватися як параметр під час роботи VM.

Крім цього, створення потоків в основному залежить від ОС, і навіть від VM залежить від впровадження.

Зараз дозвольте мені ще раз зазначити: створення ниток коштує дорого, якщо ви плануєте запускати 2000 ниток в секунду, щосекунди часу виконання. JVM не розроблений для цього . Якщо у вас буде пара стабільних працівників, яких не будуть звільняти і не вбивати знову і знову, відпочиньте.


19
"... пара стабільних працівників, яких не звільнять і не вбивають ..." Чому я почав думати про умови на робочому місці? :-)
Стівен C

6

Для створення Threadsпотрібно виділити достатню кількість пам'яті, оскільки вона повинна складати не один, а два нові стеки (один для коду Java, один для власного коду). Використання пулів виконавців / ниток дозволяє уникнути накладних витрат, повторно використовуючи теми для виконання декількох завдань для Виконавця .


@Raedwald, що таке jvm, який використовує окремі стеки?
bestsss

1
Філіп JP каже 2 стеки.
Raedwald

Наскільки я знаю, всі JVM виділяють по дві стеки на потік. Для збору сміття корисно поводитися з кодом Java (навіть коли він є JITed) інакше, ніж з безкоштовним литтям c.
Philip JF

@Philip JF Чи можете ви, будь ласка, докладно? Що ви маєте на увазі під 2 стеками: один для коду Java та один для рідного коду? Що це робить?
Гурндер

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

1

Очевидно, що суть питання полягає в тому, що означає «дороге».

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

Для цього потрібно встановити структури статусу контролю, тобто, у якому стані він працює, очікувати і т.д.

Напевно, існує велика кількість синхронізації навколо налаштування цих речей.

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