Планировщик ОС: переключение контекста
Если ты работаешь в Linux, Mac или Windows, значит, ты работаешь в ОС с упреждающим планировщиком. Это означает несколько важных вещей.
Во-первых, это означает, что планировщик непредсказуем, когда речь заходит о том, какие потоки будут выбраны для запуска в любой момент времени. Приоритеты потоков вместе с событиями, например, получение данных в сети, делают невозможным определение того, что планировщик выберет для выполнения и когда.
Во-вторых, это означает, что ты не должен писать код, основанный на некотором предполагаемом поведении потоков, которое не гарантируется каждый раз. Ты должен управлять синхронизацией и оркестровкой потоков, если тебе нужен детерминизм в приложении.
Физический акт обмена потоками на ядре называется переключением контекста. Переключение контекста происходит, когда планировщик вытягивает исполняемый поток из ядра и заменяет его исполняемым потоком. Поток, выбранный из очереди выполнения, переходит в состояние Executing. Вытянутый поток может вернуться в состояние Runnable, если он все еще может работать, или в состояние ожидания, если он был заменен из-за запроса типа IO-Bound.
Переключение контекста считается дорогим, потому что для переключения потоков между ядром требуется время. Величина задержки во время переключения контекста зависит от различных факторов, но вполне разумно, чтобы она занимала от ~1000 до ~1500 наносекунд. Учитывая, что аппаратное обеспечение должно иметь возможность разумно выполнять (в среднем) 12 инструкций в наносекунду на ядро, переключение контекста может стоить от ~ 12 000 до ~ 18 000 инструкций задержки. Твоя программа теряет возможность выполнять большое количество инструкций во время переключения контекста.
Подробнее об инструкциях ты можешь узнать в видео по ссылке.
Если у тебя есть программа, ориентированная на работу с IO-Bound, то переключение контекста будет преимуществом. Как только поток переходит в состояние ожидания, его место занимает другой поток в состоянии запуска. Это позволяет ядру всегда выполнять работу. Это один из самых важных аспектов планирования. Не позволяй ядру бездействовать, если есть работа (потоки в состоянии Runnable).
Если программа сосредоточена на работе, связанной с процессором, то переключение контекста станет кошмаром для производительности. Поскольку у thread всегда есть работа, переключение контекста останавливает эту работу. Эта ситуация резко контрастирует с тем, что происходит с рабочей нагрузкой, связанной с вводом-выводом.
Внимание! Понимание контекста очень важно в понимании работы приложений на Golang. Контекст приложений называют обычно userspace, контекст операционной системы — kernel.
Меньше потоков — больше время исполнения
В первые дни, когда процессоры имели только одно ядро, планирование не было слишком сложным. Поскольку был один процессор с одним ядром, в любой момент времени мог выполняться только один поток. Идея заключалась в том, чтобы определить период планировщика и попытаться выполнить все Runnable Threads в течение этого периода времени. Нет проблем: раздели период планирования на количество потоков, которые необходимо выполнить.
Например, если ты определяешь период планировщика как 1000 мс (1 секунду) и у тебя есть 10 потоков, то каждый поток получает 100 мс. Если у тебя есть 100 потоков, каждый поток получает 10 мс. Но что происходит, когда у тебя есть 1000 потоков? Предоставление каждому потоку кванта времени в 1 мс не работает, потому что процент времени, которое ты тратишь на переключение контекста, будет более значительным по отношению к количеству времени, которое ты тратишь на работу приложения.
Необходимо установить ограничение на то, насколько малым может быть этот временной отрезок. В последнем сценарии, если минимальный квант времени составлял 10 мс и у тебя есть 1000 потоков, период планировщика необходимо увеличить до 10000 мс (10 секунд). Если бы было 10 000 потоков, ты смотришь на период планировщика 100 000 мс (100 секунд). При 10 000 потоков с минимальным временным интервалом 10 мс для однократного запуска всех потоков в этом простом примере требуется 100 секунд, если каждый поток использует свой полный временной интервал.
Имей в виду, что это очень простой взгляд на мир. Планировщику необходимо учесть и обработать еще много данных при принятии решений о планировании. Ты контролируешь количество потоков, которые используешь в приложении. Когда есть больше потоков, которые нужно учитывать, и происходит работа, связанная с вводом-выводом, возникает больше хаоса и недетерминированного поведения. Вещи требуют больше времени для планирования и выполнения.
Вот почему правило игры гласит: «Меньше значит больше». Меньше потоков в состоянии Runnable означает меньше накладных расходов на планирование и больше времени, которое каждый поток получает с течением времени. Чем больше потоков находится в состоянии Runnable, тем меньше времени каждый поток получает с течением времени.