Комментарии 16
Спасибо,отличный материал и изложение.
Так что в итоге с пареньком, его повысили?
На самом деле, не только горутины могут утекать от использования незакрытого канала.
Если мы перестаем использовать буфферизованный канал, не удостоверившись, что он пуст и закрыт, мы также допускаем утечку сообщений. Что потенциально несет гораздо большие проблемы — некорректное логическое состояние приложения, потенциально никогда не отпущенные блокировки, потерю данных, etc.
Ну и есть ситуация, когда от закрытия канала вреда больше, чем пользы. В ситуации, когда в канал может происходить запись из большого количества горутин, бывает гораздо проще, быстрее, и надежнее, не закрывать канал вообще, а просто перестать из него читать. И использовать другой канал, чтобы сигнализировать, что первый канал больше никто не читает, и писать в него больше не надо.
О утекать могут много вещей. Не закрытый контекст, а раньше еще и не остановленные таймеры, тикеры и time.After(), пока им в одной из последний версий не сделали канал без буфера.
Утекающие контексты и таймеры - вообще весело: там структуры копеечные по размеру, и даже активня утечка память выедает крайне медленно, а вот посмотреть на количественную диаграмму кучи от pprof и там окажется один большой квадратик :).
>И использовать другой канал, чтобы сигнализировать...
Для сигнализации что "пора сворачиваться" лучше использовать контекст, ИМХО.
Однозначно. Вот только context.Done()
- это канал :) Потому что только каналы пригодны для использования в select
. К тому же контекст не рекомендуется сохранять в структурах, а вот канал от контекста вполне можно :).
Кстати про то что в size показывает unsafe - это полная ерунда. Он показывает размер указателя! Да, канал довольно жирная структура под капотом, байт под 50 размером не считая пре-аллокированного места под буфер. И поэтому любая переменная типа канал - это просто ссылка. Ее то в стек положить можно. Но вот то, на что она ссылается, чаще всего, если не всегда, будет в куче.
Да, здесь допустил неточность. У Донована тоже написано: "Как и в случае с отображением, канал является ссылкой на структуру данных, создаваемой с помощью функции make". Т.е. канал - указатель на структуру данных. Поэтому значение по умолчанию nil. Совершенно верно заметили, что Sizeof вернёт размер только указателя (https://pkg.go.dev/unsafe#Sizeof). Трассировка показала рост кучи, т.к. указатель на структуру создаётся в стеке, а вот сама структура создаётся в куче. Спасибо за замечание.
Как по мне идея писать каждым воркером в свой канал, а потом их мержить - так себе.
Вы уже даже sync.WaitGroup затащили. Кто мешает вам в главной горутине создать ОДИН канал для результатов и отдать его в горутины воркеров/писателей?
Тогда в главной горутине (а лучше в отдельной) можно дождаться wg.Wait() и после этого закрыть канал результатов. При этом читать из этого канала можно начинать не дожидаясь wg.Wait. Именно поэтому ждать окончания работ и закрывать канал лучше в отдельной горутине от горутины обрабатывающей результаты.
И да, закрывает в таком случае вовсе не писатель, но никаких проблем это не порождает т.к. канала гарантированно закроется только тогда, когда писатели закончат свою работу.
Вот мой вариант который решает ту жу задачу что и последий пример из статьи:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
run()
}
func run() {
output := make(chan string, 10)
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
for _, msg := range []string{"create user", "update user", "select user"} {
time.Sleep(10 * time.Microsecond)
output <- msg
}
}()
go func() {
defer wg.Done()
for _, msg := range []string{"create order", "update order", "select order"} {
output <- msg
}
}()
go func() {
defer wg.Done()
for _, msg := range []string{"create task", "update task", "run task"} {
output <- msg
}
}()
go func() {
wg.Wait()
close(output)
}()
for {
select {
case v, ok := <-output:
if !ok {
return
}
fmt.Println(v)
default:
fmt.Println("waiting...")
}
}
}
Только я бы еще убрал строки 47 и 48 - просто бесполезный хлам и по сути цикл без какого-то смысла. И да тут не нужен sllep - все закончит работать когда будут приняты все результаты.
Суть не сильно изменилась - закрывающая каналы горутина и пишущие в эти каналы горутины вызываются в одной и той же функции run. Здесь важно, чтобы эта логика была рядом (пусть в разных анонимных функциях, но все эти вещи вызываются в рамках одной и той же функции run), а не разнесена по нескольким функциям - run, merge.
Go: нужно ли закрывать канал?