Комментарии 38
Это основано на том же первоисточнике, но автор не упоминается, половина текста отброшена, другая переработана, и добавлена лекция об асинхронности в JS (более двух третей объема). Так что я бы сказал, что это не перевод, а обучающий доклад с использованием оригинальной статьи, как вдохновления.
type comment struct {
id int // задано
contents string // заполняем сами
children []*comment // задано
}
func loadComments(root *comment) {
var wg sync.WaitGroup
loadCommentsInner(&wg, root)
wg.Wait()
}
func loadCommentsInner(wg *sync.WaitGroup, node *comment) {
wg.Add(1)
go func() {
// не факт, что можно просто так заполнять поля произвольной структуры
// из любой горутины, но вроде можно
node.contents = loadCommentContentsByID(node.id)
wg.Done()
}()
for _, c := range node.children {
loadCommentsInner(w, c)
}
}
Нельзя сказать, что это самый простой и короткий код, который можно написать, но зато он императивный и его более-менее можно понять. Плюс, в настоящем коде всё равно ещё должна быть обработка ошибок, отмена загрузки всего запроса, и т.д.
По факту, весь код в Go в любом случае синхронный, и «асинхронная» часть здесь только в go func() {… }, которая осуществляет загрузку в отдельной горутине, не блокируя текущую.
> Почему ручное написание императивного кода для обхода дерева и выполнения действия над каждым узлом считается за благо, мне непонятно.
Это не считается за благо. Это следствие того, что Go императивный. Но императивный код легко понять большинству программистов.
> Равно как и непонятно, почему комментарий содержит в себе что ID, что содержимое (это, блин, разные сущности, я хочу отличать ID комментария от полностью загруженного комментария на уровне типов).
Вы можете это сделать и в Go, при желании. Но да, в Go нет «иммутабельных» структур данных, кроме строк.
> Или непонятно, почему комментарий отвечает за хранение своих детей
Да, ибо нет (пока что :)) дженериков.
> Да кому нужны эти деревья, можно прям так фигачить, и считатЬ, что такой код типобезопасен (Go же называют типобезопасным?).
Приведенный мной код строго типизирован. Можно было бы использовать интерфейсы или reflection, тогда, конечно, про типобезопасность можно забыть.
> Извините, я просто не ожидал, что разговор об асинхронности выльется в разговоры о моей больной теме.
Как я уже упоминал выше, в реальных приложениях кода, относящегося непосредственно к бизнес-логике не так много, так что оверхед от того, что приходится такое писать, относительно невелик. К тому же, как я сказал, поток исполнения как на ладони, и, в отличие от приведенного Вами фрагмента на Хаскеле (это библиотечный вызов же просто?), этот кусок кода можно относительно легко улучшить, добавив туда ограничение конкурентности, например.
Ранее на Python я тоже баловался и с threading и с asyncio… но только в Go создалось впечатление что ты в этом паришь и летаешь, а не на костылях ползешь на карачках как в питоне.
Ну вот, а я после Питона и JS уже боялся взяться за Go, чтобы не обжечься об асинхронность в очередной раз.
Но…
Я могу из красной всегда вызывать синие. Значит я пишу синюю функцию и пишу для неё красную примитивную обертку для всех красных мест?
Синхронную функцию и так везде можно вызвать, ей не нужна асинхронная обертка.
Проблема в том, что нельзя вызвать асинхронную из обычного кода, и обертки ей нужно писать до самого верха.
Например — у меня есть проект на синхронном фреймвоке Django, и, в функции обработки данных, которые прислал пользователь, мне нужно выполнить отложенную задачу (например, удалить какие-то данные через какое-то время).
Как вариант можно использовать модуль threading.Timer(interval, function)
, который выполнит задачу позже, но мне кажется слишком жирным запускать отдельный поток (а значит форкать в памяти весь Python-интерпретатор — слава богу, ОС хотя использует copy-on-write
для такого).
Хотя многие люди прикручивают Selery сразу, который сам по себе тот еще огромный монстр (по сравнению с одной функцией), который хранит будущие задачи в базе мать ее данных, и переодически проверяет, не пора ли чего-либо запустить.
В JS я просто делаю
setTimeout(...)
просто потому, что он изначально построен на асинхронном events-loop, что дает кучу плюшек сразу из коробки.
Надо понимать, что это движение в сторону от функциональных языков, то есть в целом в направлении от того, которое хотелось бы видеть. Почему? Функциональщики наоборот, обожают выносить все в сигнатуры методов. Может вернуть null? Одна сигнатура. Может закончится с ошибкой? Никаких эксешнов, другая сигнатура. Нужно сходить в базу? Не беда, третья сигнатура. Является асинхронным чтением с диска? Четвертая сигнатура. Причем если она ходит в базу и может вернуть null то результирующий тип будет комбинацией этих двух, аналогично с чтением с диска и возвратом результата. Тип функции всегда говорит обо всем, что происходит внутри.
Это что касается ФП. Что касается мейнстрим-языков, то они придерживаются middleground. Они в сигнатуру пихают, является ли операция синхронной или нет (с помощью Task/Promise/Future/...), и в последнее время, вернется null или нет (с помощью Option). Функции могут проводить флоу, которые никак не указаны в типе (бросать исключения/ходить в базу/..), но все же это сравнительно юзабельно (хотя и не так круто как в ФП языках).
И вот гринтреды. Завершается функция синхронно? Асинхронно? Да хрен его знает, в типе никак это не отметить. Каждый раз, когда вы отказываетесь от типизации, вы теряете часть контроля. Черт, да даже в динамичиски типизированнос жс есть промисы, и функции явно говорят о том, что они асинхронные (пусть и в рантайме), а мы в статике от этого откажемся.
У этого подхода полно минусов, но я хочу отметить один. Мне на ревью отдали JS-код который выглядел примерно так:
let result = [];
for (int i = 0; i < n; i++) {
result.push(await someOp(i))
}
я указал, что авейтиться в цикле плохо, после чего код был переписан на:
let promises = Array(n).keys().map(i => someOp(i));
let result = Promise.all(promises);
После чего время загрузи страницы с секунды упало до 200мс. Вопрос — была ли информация о том, что данная функция асинхронная полезной? Повлияло это на решение как написать вызывающий код? Имело ли это знание существенное влияние на итоговое качество продукта? Да, да и да.
Чтобы было понятно, someOp было операцией, по которой нельзя было судить, синхронная она или нет, это не было методов sendHttpRequest()
, а какой-то бизнесовый метод. И покуда вы не предлагаете кодировать все асинхронные операции венгерской нотацией и забивать на подобный вызов в цикле, гринтредовые корутины никак не показывающие себя в сигнатуре — зло.
Красные функции больнее вызывать
Тестировал функции с async и без в js — разница в несколько раз. По хорошему не сложно добавить оптимизацию, даже на уровне синтаксиса, что если внутри функции есть await — добавлять к определению функции async, если нет — убирать. Думаю async добавили как раз для того, чтобы ассинхронные функции выделялись явно, и чтобы вы думали когда пишете код. У меня на практике проблем описанных автором не возникет, так как в момент создания функции задаю себе вопрос: является ли действие, которое выполняет функция, ассинхронным по своей природе, и если да — добавляю async даже если пока нет await, если нет — не переживаю об этом, так как вся функциональность с ассинхронной природой (включая отправку логов, ожидание пользовательского ввода и т.п.) должна быть вынесена из этой функции по определению, так как single responsibility.
Подход с потоками давно известный и более старый, чем async-await. Такой же давно известный минус — блокировка данных, когда разные потоки обращаются к одним и тем же переменным. Основной плюс — потоки позволяют выполнять программу на разных ядрах и шарить память, это даёт больше возможностей для оптимизации производительности, но не нужно говорить, что это проще. Когда в продуктах топовых IT-компаний, от Apple до Google, перестанут появляться детские баги с блокировкой UI при долгих операциях — я соглашусь, что потоки стали просты. Но пока с многопоточностью почти все косячат, а с async-await даже специалисты со средней квалификацией пишут вполне работающий, без больших недостаткой, пусть и режущий old-school разработчикам глаза, код.
Паралелизм в Go это как вы выбираете моделировать вашу программу, а не цвет каждой функции в стандартной библиотеке. Это значит что вся боль пяти правил, которые я упомянул, полностью и абсолютно устранена.
А вот это не правда. Параллелизм в го, это когда цвет уже выбрали за вас, и цвет — красный. То есть вы сталкиваетесь с проблемой "красные функции больнее вызывать".
В Go, по факту, «плата» за вызов «асинхронных» функций тоже отсустствует. Вызов функции в Go дороже, чем в Си, по другой причине — потому что горутины в Go используют динамически растущий стек, но это не обязательное требование к «зеленым тредам», хотя и сильно помогает.
На самом деле сегментированный (а не просто динамически растущий) стек — это именно что обязательное требование. Без него вы никаким чудом не уложитесь в разумный объём памяти на миллионе соединений, что в свою очередь означает, что рано или поздно в языке появится асинхронная библиотека с промизами...
Автор не связывал понятие боли с накладными расходами, а связал с трудом, который необходимо будет совершить программисту (прыжок через обруч). Если в случае асинхронных функций болью являются обратные вызовы. То в случае многопоточного программирования очевидная боль проявляется в синхронизации потоков и раздельном доступе к памяти, о чем автор видимо забыл упомянуть.
Будет интересно посмотреть, к чему в итоге придёт Rust — потоки в нём были изначально (с отдельными библиотеками для большей эргономичности), а теперь вот async/await в язык понемногу вносят...
Синхронные функции возвращают результат, асинхронные — нет, взамен они вызывают коллбэк.
Возмутило то как автор приплетает, возможно для пущей убедительности, такие слова как "функция", "функция высшего порядка", "функциональщина", в то время как описывает банальный callback hell и все сопутствующие неприятности.
Функция в с е г д а возвращает результат (если отбросить возможность исключения/зависания), иначе это не функция, а процедура. А если написать кучу процедур, да ещё и сдобрить callback-ами как следует, то конечно жди проблем.
Технически, вы правы (в плане терминологии), но практически он делает ударение на том, что в синхронном коде ничего нельзя поделать с "результатом", который возвращает асинхронная функция, так как это не результат а
console.log( fetch("http://ya.ru", {method: 'GET'}) )
> Promise {<pending>}
А подписаться на него — это не «поделать»?
Что же касается самой темы, так не вижу ничего плохого, когда одни функции (синхронные) явно отделены от других (асинхронных), не возникает излишних иллюзий, и заставляет думать, как же организовать вызовы так, чтобы они действительно происходили параллельно (ну или почти параллельно), а не просто шли друг-за-дружкой асинхронной очередью
Автор рассказал лишь половину и поспешил закончить, объявив, что все озвученные проблемы решены. Но опыт подсказывает, что на фоне решения одних проблем, обычно возникают другие. Закономерный вопрос. Не является ли, в свою очередь, концепция событий и асинхронных вызовов, решением проблем многопоточного программирования? Например такой как синхронизация потоков. Там порой для решения требуется не то что через обруч прыгать, а через горящее кольцо.
Это значит что вся боль пяти правил, которые я упомянул, полностью и абсолютно устранена.
go panic()
Получил огромное удовольствие во время чтения! Спасибо за перевод и мнение после, присоединяюсь к сказанному, async и в Python, и в C# на самом деле не такие простые как кажется. В последнем "красные" функции ещё могут быть в интерфейсах, например, это добавляет ещё слой сложности.
Вот тут есть небольшая неточность:
Все, что после
await
, становится новой функцией, которую компилятор синтезирует от вашего имени
На самом деле вся функция перерабатывается в state machine.
Какого цвета ваша функция?