Goroutines 8kb и Windows OS поток 1 МБ
Как пользователь Windows, я знаю, что потоки ОС потребляют ~1 Мб памяти из-за By default, Windows allocates 1 MB of memory for each thread’s user-mode stack.
Как golang
использовать ~8 КБ памяти для каждого goroutine
, если поток ОС гораздо более прожорливым. Являются goroutine
вроде виртуальных потоков?
3 ответа
Goroutines не являются потоками, они (из спецификации):
... независимый параллельный поток управления или goroutine в одном и том же адресном пространстве.
Effective Go определяет их как:
Их называют goroutines, потому что существующие термины - потоки, сопрограммы, процессы и т. Д. - дают неточные коннотации. Goroutine имеет простую модель: это функция, выполняемая одновременно с другими goroutines в том же адресном пространстве. Он легкий и стоит немного больше, чем выделение стекового пространства. И стеки начинаются с малого, поэтому они дешевы и растут, выделяя (и освобождая) хранилище кучи по мере необходимости.
У Goroutines нет своих собственных тем. Вместо этого несколько подпрограмм мультиплексируются (могут быть) на одни и те же потоки ОС, поэтому, если один из них должен блокироваться (например, в ожидании ввода-вывода или операции блокировки канала), другие продолжают работать.
Фактическое количество потоков, выполняющих goroutines одновременно, может быть установлено с помощью runtime.GOMAXPROCS()
функция. Цитируя из runtime
пакетная документация:
Переменная GOMAXPROCS ограничивает количество потоков операционной системы, которые могут одновременно выполнять код Go на уровне пользователя. Нет ограничений на количество потоков, которые могут быть заблокированы в системных вызовах от имени кода Go; те не считаются с лимитом GOMAXPROCS.
Обратите внимание, что в текущей реализации по умолчанию только 1 поток используется для выполнения процедур.
1 MiB - это значение по умолчанию, как вы правильно заметили. Вы можете легко выбрать свой собственный размер стека (тем не менее, минимальный размер все равно намного выше, чем ~8 КБ).
Тем не менее, goroutines не являются потоками. Это просто задачи с совместной многозадачностью, похожие на Python. Сама программа - это просто код и данные, необходимые для того, что вы хотите; есть также отдельный планировщик (который запускается в одном из нескольких потоков ОС), который фактически выполняет этот код.
В псевдокоде:
loop forever
take job from queue
execute job
end loop
Конечно, execute job
часть может быть очень простой или очень сложной. Самое простое, что вы можете сделать, это просто выполнить данный делегат (если ваш язык поддерживает что-то подобное). По сути, это просто вызов метода. В более сложных сценариях могут быть такие вещи, как, например, восстановление некоторого контекста, обработка продолжений и результативность совместных задач.
Это очень легкий подход, и он очень полезен при асинхронном программировании (а это почти все в наше время:)). Многие языки теперь поддерживают нечто подобное - Python - первый, который я видел с этим ("тасклеты"), задолго до начала. Конечно, в среде без упреждающей многопоточности это было в значительной степени по умолчанию.
В C#, например, есть Task
s. Они не совсем такие же, как горутины, но на практике они подходят довольно близко - главное отличие в том, что Task
s используют потоки из пула потоков (обычно), а не отдельные выделенные потоки "планировщика". Это означает, что если вы запустите 1000 задач, они могут быть запущены 1000 отдельными потоками; на практике это потребовало бы от вас очень плохого письма Task
код (например, используя только блокировку ввода-вывода, спящие потоки, ожидание на ручках ожидания и т. д.). Если вы используете Task
Благодаря асинхронному неблокирующему вводу / выводу и работе ЦП, они довольно близки к процедурам - на практике. Теория немного другая:)
РЕДАКТИРОВАТЬ:
Чтобы прояснить ситуацию, вот как может выглядеть типичный асинхронный метод C#:
async Task<string> GetData()
{
var html = await HttpClient.GetAsync("http://www.google.com");
var parsedStructure = Parse(html);
var dbData = await DataLayer.GetSomeStuffAsync(parsedStructure.ElementId);
return dbData.First().Description;
}
С точки зрения GetData
метод, вся обработка является синхронной - это как если бы вы вообще не использовали асинхронные методы. Принципиальное отличие состоит в том, что вы не используете потоки, пока вы выполняете "ожидание"; но игнорируя это, это почти точно так же, как написание кода синхронной блокировки. Конечно, это также относится и к любым проблемам с общим состоянием - между многопоточными проблемами нет большой разницы. await
и при блокировке многопоточного ввода / вывода. Проще избежать Task
с, но только из-за инструментов, которые у вас есть, а не из-за какой-либо "магии", которая Task
с
Основным отличием от goroutines в этом аспекте является то, что в Go нет методов блокировки в обычном смысле этого слова. Вместо блокировки они ставят в очередь свой конкретный асинхронный запрос и выдают. Когда ОС (и любые другие уровни в Go - я не обладаю глубокими знаниями о внутренней работе) получает ответ, она отправляет его планировщику маршрутизации, который, в свою очередь, знает, что процедура, ожидающая ответа, теперь готов возобновить исполнение; когда он на самом деле получает слот, он продолжает работу после вызова "блокировки", как если бы он действительно блокировал, - но в действительности он очень похож на то, что в C# await
делает. Нет принципиальной разницы - между подходами C# и Go довольно много различий, но они не так уж и велики.
Также обратите внимание, что это в основном тот же подход, который использовался в старых системах Windows без упреждающей многозадачности - любой "блокирующий" метод просто возвращает выполнение потока обратно планировщику. Конечно, в этих системах у вас было только одно ядро ЦП, поэтому вы не могли выполнять несколько потоков одновременно, но принцип все тот же.
Горутины - это то, что мы называем зелеными нитями. Они не являются потоками ОС, за них отвечает планировщик go. Вот почему они могут иметь гораздо меньшие следы памяти.