[Перевод] Программист-4chan'овец
Введение
Мы используем Go для создания Dolt, первая в мире БД SQL с контролем версий. Как и большинство кодовых баз, основанных Go, мы используем каналы и горутины (от переводчика, автора этой статьи на Хабре: у меня есть хорошая статья на тему параллелизма в Go) для реализации параллелизма. Как правило мы используем эти конструкции очень скучным и обычным путем, ведь параллелизм и так сложен без всяких выдумок. Но в одном месте мы все-таки взяли маленький кусочек кода из другого open-source проекта, который использует каналы очень интересным способом: канал используется для отправки другого канала:
var c chan chan struct{}
Это канал, который отправляет другой канал, который отправляет структуру. По факту, это способ передачи каналов между разными горутинами, чтобы реализовать паттерн fan-out Думайте о «центральном» канале как о посреднике в рабочем процессе: его работа заключается в том, что бы передавать новые каналы по мере их поступления вплоть до рабочих, которые выполняют с ними реальную работу. Это работало, но идея была чересчур спецефична, с которой было трудно работать, учитывая утечки горутин. Мы переписали это, но chan chan struct{}
исчезло.
Эта идея заставила меня подумать. Насколько далеко можно зайти в этой глупой идее? Узрите то, чего не ожидали, но за чем пришли. 4-chan:
_4chan := make(chan chan chan chan int)
Зачем это
Это старая шутка из времен, когда C и подобные доминировали. Много людей долгое время не могли понять принцип указателей (pointers), наверное потому, что у них не было этого мема:
Из-за того, что указатели были сложны для понимания многим людям, а использовались они для важных и полезных вещей в C, новички иногда делали довольно глупые вещи, например: объявляли переменные следующим образом:
int****
Эти новички не понимали, какую глупость они делают, считая это признаком опыта. Таких несчастных называли »4-звездочными программистами»(4-star programmers).
Так как Go тоже поддерживает указатели, у Вас есть возможность сделать такую же вещь:
func main() { i := 1 setInt(&i) fmt.Printf("переменная i равна %d", i)
}
func setInt(i *int) { setInt2(&i)
}
func setInt2(i **int) { setInt3(&i)
}
func setInt3(i ***int) { setInt4(&i)
}
func setInt4(i ****int) { ****i = 100
}
После запуска эта программа выведет переменная i равна 100
. Вы тоже можете быть 4-звездочным программистом, это не сложно.
Но мы можем зайти дальше и использовать конструкцию, которая есть в Go, но которой нет в C: каналы. В коде они обозначены как chan
.
Понимаете к чему я веду?
Программист 4-chan’овец
Идея заключается в том, что мы будем использовать 4 слоя канала для выполнения какой-то задачи. Наш верхний уровень канала будет четвертым, поэтому обозначать его будем так:
_4chan := make(chan chan chan chan int)
(тут немного раздражает то, что Go не разрешает начинать переменные с цифры, но ничего не поделаешь, такова жизнь).
Данные, которые будем посылать в этот канал будут выглядеть так:
_3chan := make(chan chan chan int)
А в этот канал будем отправлять _2chan
и так далее до самого конца, где будем записывать int.
На каждом слое обращения к другому каналу будем создавать продьюсеров (producers) по некоторому постоянному фактору factor
. У нас это будет const factor = 3
:
func sendChanChanChan(c chan chan chan chan int) { for range factor { go func() { logrus.Debug("стартую 3chan producer") _3chan := make(chan chan chan int) sendChanChan(c, _3chan) }() }
}
И похожее для консюмеров (consumers):
func receiveChanChanChan(c chan chan chan chan int) { for _3chan := range c { logrus.Debug("получено сообщение с 4chan") for range factor { logrus.Debug("стартую 3chan consumer") go receiveChanChan(_3chan) } }
}
Продолжаем дальше:
func sendChanChan(_4chan chan chan chan chan int, _3chan chan chan chan int) { _4chan <- _3chan for range factor { go func() { logrus.Debug("стартую 2chan producer") _2chan := make(chan chan int) sendChan(_3chan, _2chan) }() }
}
И по такому принципу до sendChan
и receiveChan
.
В самом конце очереди мы высылаем фактическое значение, а не еще один канал. К этой функции (send
) будем обращаться из sendChan()
:
func send(_2chan chan chan int, _1chan chan int) { _2chan <- _1chan for range factor { go func() { logrus.Debug("стартую int producer") for range factor { go func() { logrus.Debug("отправляю число") _1chan <- 1 }() } }() }
}
Для консюмеров (consumers) мы должны что-то сделать с данными, которые получили в каналы. Давайте их сложим.
func receive(c chan int) { for s := range c { logrus.Debug("получено число") sum.Add(int32(s)) }
}
Теперь все это объединим:
const factor = 3
var sum = &atomic.Int32{}
func main() { // logrus.SetLevel(logrus.DebugLevel) _4chan := make(chan chan chan chan int) go sendChanChanChan(_4chan) go receiveChanChanChan(_4chan) time.Sleep(500 * time.Millisecond) fmt.Printf("%d ^ 5: %d", factor, sum.Load())
}
Эта программа печатает 3 ^ 5: 243
. Все правильно! Она представляет собой обобщенный способ вычисления пятой степени числа максимально распределенным способом. Вы можете потыкать это здесь (или посмотреть с подсветкой синтаксиса тут). Для более крупных факторов Вам может потребоваться увеличить длительность «засыпания» в функции time.Sleep()
.
Если Вы включите логгирование (раскомментировав первую строку в main), Вы получите примерно такой вывод, что помогает посмотреть на все слои каналов:
стартую 3chan producer стартую 2chan producer стартую 3chan consumer стартую 2chan consumer стартую chan producer стартую 1chan consumer стартую int producer получено число отправляю число
Комментарий к этому всему
Есть много причин не делать этого в реальном коде: сложность реализации и отладки, наличие небольшого самоуважения, нежелание быть забитым до смерти клавиатурами Ваших коллег и т.д.
С другой стороны, это весело делать и забавно осознавать, что это вообще работает.
Одна из лучших практических причин не отправлять каналы по каналам заключается в том, что это действительно затрудняет закрытие любого из них, что, очевидно, Вы хотели бы сделать в реальном коде, верно? В какой-то момент я фактически реализовал логику закрытия, что потребовало добавления sync.WaitGroup
буквально везде, чтобы я мог отслеживать, когда все отправленные каналы были завершены для их закрытия, но остановился на time.Sleep()
.