Comments 3
Если вы не против, покритикую:
Думаю, наиболее легкое объяснение горутинам такое: горутина (goroutine) — это легковесный оберточный функционал над потоком.
Что такое "оберточный функционал над потоком"? Оно мало того, что не понятно, так еще и искажает суть явления.
A goroutine is a lightweight thread managed by the Go runtime.
Легковесный тред, управляемый рантаймом Go. Это не обертка над потоком. Обертка над потоком - это Machine. Горутина - это штука, которая исполняется на этом потоке.
Планировщик операционной системы, в которой работает программа, переключает Машины.
Планировщик операционной системы не имеет прямого отношения к "Машинам". Он тупо запускает N (GOMAXPROCS) системных тредов, в которых они потом так и живут, ничего он не переключает. А вот планировщик в рантайме Go распределяет горутины по машинам, на которых они исполняются.
Для запуска функции как горутину необходимо написать
go func()
, где func() - функция, которую хотите запустить.
Не совсем так. go func() не запускает функцию, а всего лишь ставит горутину в очередь исполнения, передает ее планировщику, чтобы он по возможности отдал ее одной из "машин" на исполнение.
Собственно, поэтому следующее тоже не совсем верно:
Ничего не произойдет, ведь горутина выполняется параллельно с "main()"
В вашем случае с некоторой ненулевой вероятностью горутина вообще не выполняется, тупо в очереди находится (тем более в go playground, который, возможно, вообще однопоточный)
// создаем WaitGroup
wg := &sync.WaitGroup{}
Вот тут & излишне. Дальше ваша wg передается замыканием, а замыкание в Go проходит по указателю.
"mutual exclusion", что в переводе означает "взаимное изолирование"
В переводе это означает "взаимное исключение"
это некая примитивная синхронизация
Это вы так synchronization primitive перевели? Не "некая примитивная синхронизация", а "примитив синхронизации". Как, к слову, и WaitGroup и даже, внезапно, канал.
Перед обновлением счетчика, мы вызываем "lock.Lock()" для создания блокировки. Теперь ни одна другая горутина не сможет использовать "counter".
Ну вы бы хоть рассказали, почему не сможет. Это примитив синхронизации. Он заблокирует горутину (да, он не значение блокирует, а горутину), пока инструкция не сможет быть исполнена.
Т.е. если вы лочите мьютекс из горутины №1, а потом пытаетесь сделать то же самое из горутины №2, то вторая горутина заблокируется, пока первая не разлочит мьютекс.
Кстати, раз уж вы решили разложить все по полочкам, то примитивы синхронизации надо было начинать с атомиков. Потому что у мьютекса и вейтгруппы внутри как раз они и есть.
Атомарные операции - это низкоуровневые примитивы синхронизации, которые обеспечивают способ выполнения операций чтения-модификации-записи (read-modify-write) над общими переменными без необходимости в блокировке (Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена).
Ну вот, в формулировке read-modify-write, а в примере ниже save/load. Не надо так.
Ну и вот это "Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе" - это скорее к теории баз данных формулировка, и это про транзакционность.
В трактовке многопоточных приложений - a sequence of instructions that are executed as a single, indivisible unit of work
Последовательность инструкций, исполняемых как одна неделимая единица работы. Т.е. это гарантия, что в процессе выполнения не произойдет переключение контекста.
Например i++ не атомарна. Это получение значения переменной + увеличение значения на 1 + запись значения в переменную - 3 операции, соседний поток может вклиниться между любой из них, что приведет к неконсистентности данных. Атомарные операции отличаются тем, что соседний поток вклиниться не может, они для того и нужны.
Технически - это конвейер, откуда можно считывать или помещать данные. То есть одна горутина может отправить данные в канал, а другая — считать помещенные в этот канал данные.
Ничего не понятно. Технически канал - примитив синхронизации, по сути - семафор.
Канал можно и закрыть.
Канал нужно закрывать. Просто за правило возьмите: как только записали в канал все, что хотели - закройте канал.
Для этого надо вызвать функцию
close(Ваш канал)
, тем самым заблокировав доступ к чтению из него.
В смысле, заблокировав доступ к чтению??? Чтение из закрытого канала возвращает zero-value для типа канала. С чего оно заблокируется-то?
val, ok := <- Ваш канал
, где val - переменная, в которую запишется значение с канала, если это возможно, а ok - булева переменная, где true означает, что из канала можно прочитать данные, а false - что он закрыт/невозможно считать.
Ну доку-то почитайте! false означает, что канал закрыт (и все значения из него прочитаны, в случае буферизованного канала). Никаких невозможно читать там нет, даже напротив, читать из него очень даже возможно.
С помощью for range можно читать данные из закрытого буферизированного канала, так как данные остаются в буфере даже после закрытия канала.
А без range'а нельзя? Данные в буфере пропадают? Что вы несете?
Если Вы будете считывать данные из пустого канала, Вы получите ошибку "Deadlock", которая появляется при бесконечном считывании из канала.
Дичь. Эталонная. Дедлок - это ситуация, когда все горутины находятся в спящем состоянии и не могут быть "разбужены", тчк.
select
- это почти чтоswitch
, но без аргументов.
Гениальное определение. Неверное, ничего не объясняющее, но гениальное.
Вот такое будет ближе к истине:
Примитив синхронизации, блокирующий поток исполнения, пока один из case'ов не сможет быть выполнен.
Я долго писал эту статью, вытаскивая из каждого источника самое важно
Достаточно было одного источника, https://go.dev/doc/
Если вы не против, покритикую:
Думаю, наиболее легкое объяснение горутинам такое: горутина (goroutine) — это легковесный оберточный функционал над потоком.
Что такое "оберточный функционал над потоком"? Оно мало того, что не понятно, так еще и искажает суть явления.
A goroutine is a lightweight thread managed by the Go runtime.
Легковесный тред, управляемый рантаймом Go. Это не обертка над потоком. Обертка над потоком - это Machine. Горутина - это штука, которая исполняется на этом потоке.
Планировщик операционной системы, в которой работает программа, переключает Машины.
Планировщик операционной системы не имеет прямого отношения к "Машинам". Он тупо запускает N (GOMAXPROCS) системных тредов, в которых они потом так и живут, ничего он не переключает. А вот планировщик в рантайме Go распределяет горутины по машинам, на которых они исполняются.
Для запуска функции как горутину необходимо написать
go func()
, где func() - функция, которую хотите запустить.
Не совсем так. go func() не запускает функцию, а всего лишь ставит горутину в очередь исполнения, передает ее планировщику, чтобы он по возможности отдал ее одной из "машин" на исполнение.
Собственно, поэтому следующее тоже не совсем верно:
Ничего не произойдет, ведь горутина выполняется параллельно с "main()"
В вашем случае с некоторой ненулевой вероятностью горутина вообще не выполняется, тупо в очереди находится (тем более в go playground, который, возможно, вообще однопоточный)
// создаем WaitGroup
wg := &sync.WaitGroup{}
Вот тут & излишне. Дальше ваша wg передается замыканием, а замыкание в Go проходит по указателю.
"mutual exclusion", что в переводе означает "взаимное изолирование"
В переводе это означает "взаимное исключение"
это некая примитивная синхронизация
Это вы так synchronization primitive перевели? Не "некая примитивная синхронизация", а "примитив синхронизации". Как, к слову, и WaitGroup и даже, внезапно, канал.
Перед обновлением счетчика, мы вызываем "lock.Lock()" для создания блокировки. Теперь ни одна другая горутина не сможет использовать "counter".
Ну вы бы хоть рассказали, почему не сможет. Это примитив синхронизации. Он заблокирует горутину (да, он не значение блокирует, а горутину), пока инструкция не сможет быть исполнена.
Т.е. если вы лочите мьютекс из горутины №1, а потом пытаетесь сделать то же самое из горутины №2, то вторая горутина заблокируется, пока первая не разлочит мьютекс.
Кстати, раз уж вы решили разложить все по полочкам, то примитивы синхронизации надо было начинать с атомиков. Потому что у мьютекса и вейтгруппы внутри как раз они и есть.
Атомарные операции - это низкоуровневые примитивы синхронизации, которые обеспечивают способ выполнения операций чтения-модификации-записи (read-modify-write) над общими переменными без необходимости в блокировке (Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена).
Ну вот, в формулировке read-modify-write, а в примере ниже save/load. Не надо так.
Ну и вот это "Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе" - это скорее к теории баз данных формулировка, и это про транзакционность.
В трактовке многопоточных приложений - a sequence of instructions that are executed as a single, indivisible unit of work
Последовательность инструкций, исполняемых как одна неделимая единица работы. Т.е. это гарантия, что в процессе выполнения не произойдет переключение контекста.
Например i++ не атомарна. Это получение значения переменной + увеличение значения на 1 + запись значения в переменную - 3 операции, соседний поток может вклиниться между любой из них, что приведет к неконсистентности данных. Атомарные операции отличаются тем, что соседний поток вклиниться не может, они для того и нужны.
Технически - это конвейер, откуда можно считывать или помещать данные. То есть одна горутина может отправить данные в канал, а другая — считать помещенные в этот канал данные.
Ничего не понятно. Технически канал - примитив синхронизации, по сути - семафор.
Канал можно и закрыть.
Канал нужно закрывать. Просто за правило возьмите: как только записали в канал все, что хотели - закройте канал.
Для этого надо вызвать функцию
close(Ваш канал)
, тем самым заблокировав доступ к чтению из него.
В смысле, заблокировав доступ к чтению??? Чтение из закрытого канала возвращает zero-value для типа канала. С чего оно заблокируется-то?
val, ok := <- Ваш канал
, где val - переменная, в которую запишется значение с канала, если это возможно, а ok - булева переменная, где true означает, что из канала можно прочитать данные, а false - что он закрыт/невозможно считать.
Ну доку-то почитайте! false означает, что канал закрыт (и все значения из него прочитаны, в случае буферизованного канала). Никаких невозможно читать там нет, даже напротив, читать из него очень даже возможно.
С помощью for range можно читать данные из закрытого буферизированного канала, так как данные остаются в буфере даже после закрытия канала.
А без range'а нельзя? Данные в буфере пропадают? Что вы несете?
Если Вы будете считывать данные из пустого канала, Вы получите ошибку "Deadlock", которая появляется при бесконечном считывании из канала.
Дичь. Эталонная. Дедлок - это ситуация, когда все горутины находятся в спящем состоянии и не могут быть "разбужены", тчк.
select
- это почти чтоswitch
, но без аргументов.
Гениальное определение. Неверное, ничего не объясняющее, но гениальное.
Вот такое будет ближе к истине:
Примитив синхронизации, блокирующий поток исполнения, пока один из case'ов не сможет быть выполнен.
Я долго писал эту статью, вытаскивая из каждого источника самое важно
Достаточно было одного источника, https://go.dev/doc/
Погружение в параллелизм:
Ожидание - memory models, happens-before, structured concurrency, model checking/верификация по csp
Реальность - time.Sleep, mutex, atomic
Погружение в параллелизм в Go