Pull to refresh

Comments 38

Я уже где-то читал иной перевод этой же статьи, про цветные функции.

Это основано на том же первоисточнике, но автор не упоминается, половина текста отброшена, другая переработана, и добавлена лекция об асинхронности в JS (более двух третей объема). Так что я бы сказал, что это не перевод, а обучающий доклад с использованием оригинальной статьи, как вдохновления.

UFO just landed and posted this here
Мне кажется, автор не утверждал, что Go это всё изобрел :). Библиотеки для «зеленых тредов» существовали ещё для старых UNIX-систем, однако всегда были нюансы. В Go просто это всё реализовано довольно хорошо, ну и язык компилируемый, поэтому в целом может соревноваться по скорости с C. Erlang, появившийся очень давно, и где тоже всё хорошо в плане асинхронщины, это скриптовый язык, да ещё и функциональный, что сразу делает его намного менее привлекательным для многих разработчиков (включая меня). Мне кажется, Go получился таким, какой он есть, потому что он расчитан на массовую аудиторию, и вроде как вполне неплохо справляется со своими обязанностями.
UFO just landed and posted this here
Что подразумевается под «с минимальной писаниной… загрузив все их асинхронно»? Каждый комментарий асинхронно по отдельности? Если да, то примерно такой код должен сделать то, что вы хотите (хотя лучше ограничивать конкурентность, что тоже несложно в Go):

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() {… }, которая осуществляет загрузку в отдельной горутине, не блокируя текущую.
UFO just landed and posted this here
Скажу честно, не очень хорошо знаком с функциональщиной, поэтому аргументированно вряд ли могу рассуждать на тему того, какой подход лучше :). Реальное приложение на Go будет выглядеть совершенно по-другому, и логика асинхронной загрузки будет хорошо если 10% кода составлять. Остальное это управление конкуретностью (чтобы не загружать в слишком много потоков), обработка ошибок асинхронной загрузки (в каждом конкретном случае мы должны решать, нормально ли отдавать пользователю неполный результат или нет), логирование, трассировка, и т.д. Плюс вряд ли у вас будет где-то существовать дерево комментариев с их id, так что структуры данных тоже будут использоваться другие, хотя и наверняка похожие.

> Почему ручное написание императивного кода для обхода дерева и выполнения действия над каждым узлом считается за благо, мне непонятно.
Это не считается за благо. Это следствие того, что Go императивный. Но императивный код легко понять большинству программистов.

> Равно как и непонятно, почему комментарий содержит в себе что ID, что содержимое (это, блин, разные сущности, я хочу отличать ID комментария от полностью загруженного комментария на уровне типов).
Вы можете это сделать и в Go, при желании. Но да, в Go нет «иммутабельных» структур данных, кроме строк.

> Или непонятно, почему комментарий отвечает за хранение своих детей
Да, ибо нет (пока что :)) дженериков.

> Да кому нужны эти деревья, можно прям так фигачить, и считатЬ, что такой код типобезопасен (Go же называют типобезопасным?).
Приведенный мной код строго типизирован. Можно было бы использовать интерфейсы или reflection, тогда, конечно, про типобезопасность можно забыть.

> Извините, я просто не ожидал, что разговор об асинхронности выльется в разговоры о моей больной теме.
Как я уже упоминал выше, в реальных приложениях кода, относящегося непосредственно к бизнес-логике не так много, так что оверхед от того, что приходится такое писать, относительно невелик. К тому же, как я сказал, поток исполнения как на ладони, и, в отличие от приведенного Вами фрагмента на Хаскеле (это библиотечный вызов же просто?), этот кусок кода можно относительно легко улучшить, добавив туда ограничение конкурентности, например.
UFO just landed and posted this here
Я когда на Go попробовал поиграть в асинхронность, так сразу влюбился в этот язык.

Ранее на Python я тоже баловался и с threading и с asyncio… но только в Go создалось впечатление что ты в этом паришь и летаешь, а не на костылях ползешь на карачках как в питоне.

Ну вот, а я после Питона и JS уже боялся взяться за Go, чтобы не обжечься об асинхронность в очередной раз.

В Go вам не нужно думать о том, асинхронный код у вас «под капотом» или нет. Вы всегда пишете синхронный код. Единственный случай, когда это становится важно, это когда вы вызываете из Go кода библиотеки на Си, которые сами ходят в сеть (блокирующим образом), но даже в этом случае просто автоматически создаются новые треды (по количеству заблокированных на I/O вызовов), и всё более-менее хорошо.

Но…
Я могу из красной всегда вызывать синие. Значит я пишу синюю функцию и пишу для неё красную примитивную обертку для всех красных мест?

Судя по всему, также важно, какие функции передаются через аргументы. То есть, красная функция не может вызвать синию, передавая красную функцию одним из аргументов.

Ну как сказать: вызвать-то может, только синяя не вправе вызывать этот аргумент.

Синхронную функцию и так везде можно вызвать, ей не нужна асинхронная обертка.
Проблема в том, что нельзя вызвать асинхронную из обычного кода, и обертки ей нужно писать до самого верха.


Например — у меня есть проект на синхронном фреймвоке 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(), а какой-то бизнесовый метод. И покуда вы не предлагаете кодировать все асинхронные операции венгерской нотацией и забивать на подобный вызов в цикле, гринтредовые корутины никак не показывающие себя в сигнатуре — зло.

UFO just landed and posted this here

Понятно почему. В мире Go полиморфизм — от лукавого.
Зато простыни кода с wg.Add(1) и wg.Done() считаются чем-то само собой разумеющимся.

Основная проблема в этом пункте:
Красные функции больнее вызывать

Тестировал функции с 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>}
Как это ничего нельзя поделать с «результатом» (обещанием)?
А подписаться на него — это не «поделать»?

Я имею в виду нельзя ничего поделать здесь и сейчас в синхронном смысле. А подписаться конечно, можно (+callback) и заодно передать в параметрах функцию обработки результата (+callback) и т.п. — здравствуй Callback Hell или .then().then().then()

Если немного задумываться над потоками выполнения, то можно избегать и callbackHell (очень в этом плане спасала либа async) и длинной цепочки then-ов.

Это не та проблема, для которой нет решения =)
Читая про «красные» и «синие» функции, я сперва подумал, что автор ведёт речь про «чистые» и с «сайд-эффектами», а никак не про синхронные и асинхронные =)

Что же касается самой темы, так не вижу ничего плохого, когда одни функции (синхронные) явно отделены от других (асинхронных), не возникает излишних иллюзий, и заставляет думать, как же организовать вызовы так, чтобы они действительно происходили параллельно (ну или почти параллельно), а не просто шли друг-за-дружкой асинхронной очередью
Чего только не придумаю, лишь бы программист про монады не догадался…

Автор рассказал лишь половину и поспешил закончить, объявив, что все озвученные проблемы решены. Но опыт подсказывает, что на фоне решения одних проблем, обычно возникают другие. Закономерный вопрос. Не является ли, в свою очередь, концепция событий и асинхронных вызовов, решением проблем многопоточного программирования? Например такой как синхронизация потоков. Там порой для решения требуется не то что через обруч прыгать, а через горящее кольцо.

Это значит что вся боль пяти правил, которые я упомянул, полностью и абсолютно устранена.

go panic()

А можно для не-гошников объяснить какое поведение будет?

Получил огромное удовольствие во время чтения! Спасибо за перевод и мнение после, присоединяюсь к сказанному, async и в Python, и в C# на самом деле не такие простые как кажется. В последнем "красные" функции ещё могут быть в интерфейсах, например, это добавляет ещё слой сложности.

Вот тут есть небольшая неточность:

Все, что после await, становится новой функцией, которую компилятор синтезирует от вашего имени

На самом деле вся функция перерабатывается в state machine.

Sign up to leave a comment.

Articles