Если планировщику разрешён только один одновременно исполняющийся поток Go, могут ли две горутины выполнять работу конкурентно?
Да, могут. При одном исполняющемся потоке горутины не работают параллельно в один и тот же момент, но планировщик может переключать их выполнение, поэтому они выполняются конкурентно — частями, чередуясь во времени.
Лёгкие горутины позволяют описывать множество независимых задач без создания отдельного потока операционной системы для каждой задачи. Такой подход отделяет количество логических задач от количества физических процессоров и потоков, доступных для выполнения.
Поэтому в Go важно различать конкурентность и параллелизм: первая описывает структуру и чередование задач, второй — их одновременное выполнение на нескольких вычислительных ресурсах.
Ошибка возникает, когда наличие нескольких горутин автоматически принимают за одновременное выполнение. При одном исполняющемся потоке две горутины не могут одновременно исполнять инструкции, но каждая может получить управление после блокировки другой горутины, системного события или решения планировщика.
Если требуется ускорить вычисления за счёт реального использования нескольких ядер, одной конкурентной структуры недостаточно. Нужно, чтобы среда выполнения могла одновременно исполнять горутины на нескольких потоках; при этом доступ к общему состоянию всё равно требует синхронизации.
Конкурентное выполнение означает, что несколько задач находятся в процессе работы и могут продвигаться по очереди. Параллельное выполнение означает, что инструкции разных задач исполняются одновременно на разных вычислительных ресурсах.
При одном потоке Go планировщик выбирает готовую горутину, выполняет часть её работы, а затем может переключиться на другую. Переключение особенно вероятно, когда горутина блокируется на канале, мьютексе, таймере или другом событии. Явная блокировка не обязательна: планировщик также может вытеснить горутину, чтобы другие получили возможность выполняться.
Если одна горутина выполняет долгий непрерываемый участок, остальные могут ждать дольше. Поэтому конкурентность не гарантирует равномерного чередования, определённого порядка выполнения или ускорения CPU-bound вычислений.
Параллелизм появляется, когда одновременно доступны несколько исполняющих потоков Go и операционная система может разместить их на разных ядрах. Увеличение числа потоков не всегда ускоряет программу: накладные расходы планирования, конкуренция за память, блокировки и пропускная способность CPU могут стать ограничениями.
Сервис обрабатывает независимые CPU-bound задачи. При одном исполняющемся потоке разработчик запускает по горутине на каждую задачу и ожидает сокращения времени обработки. Горутины действительно помогают организовать работу, но задачи лишь чередуются и не получают реального параллельного исполнения.
Вариант с одной горутиной проще и может иметь меньшие накладные расходы, но хуже подходит для задач, которые блокируются на вводе-выводе. Вариант с большим числом горутин удобен для конкурентной обработки, однако сам по себе не создаёт параллелизм и может увеличить давление на планировщик.
Практическое решение — разделить модель выполнения и ограничение ресурсов: запускать горутины для независимых задач, подобрать число одновременно исполняемых вычислительных workers под нагрузку и измерить результат профилированием. Для задач, ожидающих сеть или диск, конкурентность часто повышает загрузку ресурсов даже без параллельного исполнения CPU-инструкций.
1. Вопрос: означает ли конкурентность гарантированное чередование горутин?
Нет. Конкурентность означает возможность продвижения нескольких задач во времени, но не задаёт строгую очередность, равное время выполнения или обязательное переключение после каждой инструкции. Нельзя строить корректность программы на предполагаемом порядке планирования; для порядка нужны каналы или другие примитивы синхронизации.
2. Вопрос: ускорит ли добавление горутин любую программу при одном исполняющемся потоке?
Нет. Для CPU-bound работы горутины при одном потоке обычно только организуют чередование и добавляют накладные расходы. Для задач с блокирующим ожиданием они могут повысить общую пропускную способность, потому что пока одна горутина ждёт, другая может использовать поток.
3. Вопрос: решает ли параллельное выполнение проблему гонки данных?
Нет. Параллельность лишь увеличивает возможность одновременного доступа к общей памяти, но гонка возможна и при конкурентном чередовании на одном потоке. Общие данные нужно защищать мьютексом, каналом, атомарными операциями или другой подходящей схемой синхронизации; выбор зависит от характера доступа и требуемой производительности.