Поток выполнения (native/kernel thread) — это часть процесса, в которой инструкции могут выполняться независимо и иметь доступ к общим ресурсам. За управление потоками отвечает планировщик ОС.

Многопоточность — свойство железа и софта, при котором несколько потоков могут выполняться параллельно, не мешая друг другу. Если разработчики ПО сумели эффективно распараллелить отдельные части программы, можно рассчитывать на увеличение производительности, кратное количеству доступных ядер процессора.

Но для реализации работы нескольких потоков требуется как аппаратная поддержка, так и программная. Под аппаратной подразумевается наличие выделенных ядер в процессоре под каждый поток ОС (по два с hyper-threading), под программной — поддержка многопоточности ОС и конструкциями языка.

Один из первых нюансов, с которым сталкиваются разработчики при проектировании многопоточного ПО, — организация доступа к общим ресурсам, а конкретно — к памяти. Неверное разделение доступов между потоками может привести к порче данных. Например, если два потока одновременно и параллельно пишут своё значение в одну и ту же переменную, какое из значений будет записано в результате? Да и в целом необходимость мыслить несколькими потоками осложняет понимание процесса выполнения программы.

Средствами языка разработчикам хотелось избавиться от головной боли, которая может возникнуть при переходе на многопоточное программирование. Разные языки предлагали разные решения этой проблемы, но так как большинство существовавших на тот момент языков создавались без учёта многопоточности, её поддержку приделывали «сбоку».

В интерпретируемых языках программирования, занимавших львиную долю рынка того времени, были дополнительные сложности. Нужно было организовать механизмы обращения к глобальному состоянию интерпретатора из нескольких потоков и обмен данными между этими потоками. Стало понятно, что необходим новый язык программирования, который бы был изначально создан с расчётом на многопоточность и позволял быстро и удобно писать приложения, использующие доступные ядра процессоров.

Для эффективного использования доступной вычислительной мощности и потоков ввода/вывода#Go оперирует несколькими системными потоками, распределяя между ними ещё больше своих собственных легковесных потоков со стратегией m*n. То есть на одном системном потоке могут исполняться несколько горутин. Если системный поток блокирован ожиданием ввода/вывода или перегружен, диспетчер Go может перенести горутину на свободный. Если захваченных системных потоков недостаточно, диспетчер Go может потребовать у системы новых.

Возможность спроектировать язык с нуля позволила на фундаментальном уровне внедрить многие решения, чтобы разработчики могли писать многопоточный код. Существуют разные модели многопоточных вычислений, среди которых архитекторы Go выбрали модель CSP (Communicating Sequential Processes), описанную Энтони Хоаром в одноимённой статье. В этой модели программа представляет собой множество одновременно работающих подзадач, которые общаются между собой, передавая сообщения через каналы связи.

image

  • Подзадача в Go называется горутиной. Горутины живут в прослойке между программой и средой выполнения под названием runtime. Помимо прочего, рантайм отвечает за организацию конкурентного доступа множества горутин к общему ограниченному ресурсу — процессорному времени. Его задача — распределить этот ресурс так, чтобы каждая горутина смогла поработать хотя бы какое-то время, прежде чем управление перейдёт к следующей. Таким образом достигается иллюзия параллельности выполнения задач при количестве горутин, многократно превышающем количество доступных системных потоков.

Note

В отличие от event loop, код горутины пишется и выглядит последовательным и самостоятельным, без всяких callback-вызовов, как в случае с горутиной в виде отдельной программы.

  • Горутины пишутся и выполняются как самостоятельные потоки вычислений, но для большей совместной эффективности горутины могут взаимодействовать между собой, обмениваясь сообщениями. Каналом связи для передачи сообщений в Go выступает такая абстракция, как channel (канал). На каналах построены все механизмы обмена и синхронизации потоков в Go. Канал — это подобие туннеля, в который одна горутина может «положить» значение, а другая — это значение оттуда «вытащить» и что-нибудь с ним сделать.

Обе абстракции — не подключаемые библиотеки или фреймворки, а встроенные конструкции синтаксиса, которые автоматически становятся одной из «киллер-фич» языка и большим преимуществом для программиста. В этом плане Go сильно оторвался от конкурентов.

Например, в популярном языке Python ближайшим аналогом горутин можно считать класс Process, а функцию Pipe() из модуля multiprocessing стандартной библиотеки — аналогом канала. Разница в том, что в Python многопоточность не реализована в базовом синтаксисе, и Process захватывает тяжеловесный системный процесс полностью в единоличное пользование. Лёгкие потоки с дешёвым переключением контекста симулируются в Python классом Thread из модуля threading. Но Thread в эталонной реализации CPython не добавляет параллелизма и не умеет использовать все ядра процессора. Одновременно может исполняться только один Thread, поэтому каналы обмена данными между Thread просто не нужны.


📂 Go | Последнее изменение: 12.08.2024 09:42