Базовый CS с нуля
Конкурентность и параллелизм
Цикл событий из прошлого урока обрабатывает много задач — много колбэков, от многих устройств — и создаёт ощущение, что программа делает несколько вещей «сразу». Веб-сервер с циклом событий обслуживает тысячи пользователей будто бы вместе.
Но присмотрись к трассировке из урока 03. На каждом отдельном шаге выполнялась ровно одна вещь: одна синхронная строка или один колбэк, никогда не две. Цикл событий жонглирует многими задачами, но выполняет их строго по одной за раз, по очереди.
Так что «сразу» прячет две по-настоящему разные идеи. Одна — много задач, выполняющихся по очереди на одном процессоре. Другая — много задач, по-настоящему выполняющихся вместе на нескольких процессорах. Их легко спутать и важно разделить. Этот урок — последний в треке — даёт каждой имя: конкурентность и параллелизм.
После этого урока ты сможешь дать определение ядра и потока, дать определение конкурентности как чередования задач на одном ядре, так что они выполняются по очереди, дать определение параллелизма как задач, выполняющихся в один миг на нескольких ядрах, объяснить, почему одноядерная машина может быть конкурентной, но никогда параллельной, и сформулировать слоган «конкурентность — это структура, параллелизм — это исполнение».
Ядро — это один движок выборки–декодирования–исполнения. Вспомни цикл выборки–декодирования–исполнения из Блока 03. Ядро — это одна полная копия этого механизма: один движок, который выбирает одну инструкцию, декодирует её, исполняет и повторяет. Ядро делает ровно одну инструкцию за раз — такова его природа.
У ранних процессоров было одно ядро: один движок, значит, одна инструкция в полёте, когда-либо. Современные процессоры многоядерные: чип содержит несколько ядер — четыре, восемь, больше — каждое полный движок выборки–декодирования–исполнения, каждое выполняет свой собственный поток инструкций независимо от других.
Число ядер — твёрдый физический факт о машине. Это потолок того, сколько инструкций может физически исполняться в один и тот же миг. Одно ядро: одна инструкция за раз, точка. Восемь ядер: до восьми инструкций в один миг.
Поток — это один стрим инструкций. Поток — это единый, упорядоченный стрим исполняемых инструкций — один счётчик команд, продвигающийся по коду, один стек вызовов с кадрами (Блок 08). Всё трассированное до сих пор в этом треке было одним потоком: один счётчик команд, один стек, одна инструкция за другой.
Важное различие: поток — это стрим работы; ядро — это оборудование, выполняющее стрим. Ядро выполняет один поток в данный миг. У программы может быть много потоков — много отдельных стримов работы — но они физически продвигаются, только когда ядро их выполняет.
Так что в картине два слоя. Сколько потоков у программы — выбор программный. Сколько ядер у машины — факт аппаратный. Отношение между этими двумя числами — ровно то, что отделяет конкурентность от параллелизма.
Конкурентность: много задач, чередуемых на одном ядре, по очереди. Конкурентность — это работа со многими задачами через их чередование: ядро выполняет кусочек задачи A, затем кусочек задачи B, затем снова A — переключаясь между ними достаточно быстро, чтобы все они продвигались со временем. В любой отдельный миг выполняется только одна задача; задачи выполняются по очереди.
Это ровно цикл событий из урока 03. Одно ядро, один поток, но много колбэков, все «в процессе». Цикл выполняет один колбэк до конца, затем следующий, затем следующий. Программа продвигается по многим задачам, но в один момент выполняется только одна. Это конкурентность: много задач, структурированных так, чтобы их можно было чередовать на одном ядре.
Конкурентность не требует нескольких ядер. Одноядерная машина полностью конкурентна: она просто быстро переключается между задачами. Конкурентность — это про то, как организована работа, чтобы одно ядро могло держать много задач в движении — это свойство структуры программы.
Параллелизм: много задач в один миг на нескольких ядрах. Параллелизм — это много задач, буквально выполняющихся в один миг — а это требует нескольких ядер. Ядро 1 исполняет инструкцию задачи A, пока, в тот же самый миг, ядро 2 исполняет инструкцию задачи B. Две инструкции по-настоящему в полёте вместе, потому что есть два движка.
Параллелизм нельзя подделать на одном ядре. Одно ядро — это один движок выборки–декодирования–исполнения; он физически исполняет одну инструкцию за раз. Никакая скорость переключения этого не меняет — переключение даёт тебе конкурентность (выполнение по очереди), но никогда параллелизм (настоящую одновременность). Параллелизму нужно оборудование: столько инструкций в один миг, сколько есть ядер.
Отсюда слоган: конкурентность — про структуру; параллелизм — про исполнение. Конкурентность — это как ты организуешь задачи, чтобы их можно было чередовать — проектное решение в программе. Параллелизм — это действительно ли они выполняются в один миг — факт об оборудовании, делающем выполнение. Программа может быть конкурентной на одном ядре; она параллельна, только когда несколько ядер выполняют её задачи вместе.
Классификация четырёх ситуаций.
1 — Одноядерная машина выполняет цикл событий со 100 колбэками в очереди. Одно ядро, значит, одна инструкция за раз. 100 колбэков выполняются по очереди через цикл. Это конкурентность, не параллелизм: много задач чередуются, одно ядро, никогда не две в один миг.
2 — 8-ядерная машина выполняет 8 отдельных потоков, по одному на ядро, все вычисляют сразу. Восемь ядер, восемь инструкций в один миг. Это параллелизм: задачи буквально выполняются вместе. (Это также конкурентно — работа структурирована как много задач — но определяющий новый факт — настоящая одновременность.)
3 — Одноядерную машину просят выполнить 2 задачи «параллельно». Невозможно, как сказано. Одно ядро исполняет одну инструкцию за раз. Машина может быть конкурентной (чередовать 2 задачи, по очереди), но никогда не может быть параллельной — для этого нужно второе ядро.
4 — Программа всего с одной задачей, одним потоком, выполняющаяся на 8-ядерной машине. Один стрим работы использует одно ядро. Остальные 7 ядер простаивают. Это ни то, ни другое — ни конкурентность, ни параллелизм в сколько-нибудь осмысленном смысле — есть только одна задача. Лишние ядра ничего не дают программе, не структурированной во много задач.
Закономерность: конкурентность решается структурой программы (разбита ли работа на чередуемые задачи?); параллелизм решается оборудованием (выполняют ли несколько ядер эти задачи в один миг?).
Частая ошибка
Частая ошибка — считать «конкурентный» и «параллельный» синонимами или полагать, что больше ядер автоматически делают программу быстрее. Они разные, и нет, не делают. Программа, написанная как одна задача с одним потоком, выполняется ровно на одном ядре, сколько бы их ни было у машины — лишние ядра простаивают. Параллелизм помогает, только если программа сперва структурирована конкурентно во много задач. Структура идёт первой; только затем оборудование может выполнить кусочки параллельно.
Одно ядро исполняет сколько инструкций в один миг?
Цикл событий на одноядерной машине чередует 50 колбэков, по очереди. Сколько из этих колбэков выполняется в один и тот же миг?
Параллелизм означает задачи, выполняющиеся в один миг. Чтобы выполнить 4 задачи по-настоящему параллельно, сколько минимум ядер нужно машине?
Одноядерная машина выполняет много чередуемых задач. Введи 1, если это конкурентность, 2, если это параллелизм.
Программа — это одна задача с одним потоком, выполняющаяся на 16-ядерной машине. Сколько ядер на самом деле делают работу этой программы?
В чём разница между конкурентностью и параллелизмом?
Ядро — это один движок выборки–декодирования–исполнения; он выполняет ровно одну инструкцию за раз. Поток — это один упорядоченный стрим инструкций — один счётчик команд, один стек вызовов. Конкурентность — это работа со многими задачами через их чередование на одном ядре: задачи выполняются по очереди, и только одна выполняется в любой миг — цикл событий есть конкурентность. Параллелизм — это много задач, буквально выполняющихся в один миг, что требует нескольких ядер, по одной инструкции на ядро в полёте вместе. Одноядерная машина может быть полностью конкурентной, но никогда параллельной. Слоган: конкурентность — про структуру (как организована работа в чередуемые задачи) и параллелизм — про исполнение (действительно ли оборудование выполняет эти задачи в один миг).
Это последний урок трека Base CS. Проследи путь, который ты прошёл. Блок 01: биты — всё есть узоры из двух символов. Блоки 02–03: эти биты лежат в памяти, а процессор выполняет над ними цикл выборки–декодирования–исполнения. Блок 04: этот машинный код — то, во что компилируется твой язык. Блоки 05–06: значения, типы и переменные — именованные ячейки, хранящие состояние. Блоки 07–08: поток управления направляет счётчик команд, а функции складывают кадры на стек вызовов. Блоки 09–10: данные размещены в памяти, а абстракция позволяет перестать думать о слоях ниже. Блок 11: когда программа падает, стек трассировки и неопределённое поведение становятся читаемыми, а не загадочными. И Блок 12: поскольку CPU быстрый, а устройства медленные, программы устроены с async, колбэками, циклом событий и ясным пониманием конкурентности против параллелизма. Компьютер больше не чёрный ящик. Это быстрая, простая машина, выполняющая цикл выборки–декодирования–исполнения над битами — и каждый слой выше, вплоть до программы, которую ты пишешь, есть то, о чём ты теперь можешь рассуждать, шаг за шагом.