_>На C# для такой задачи ПМ только усложнять задачу. Вот полная консольная программа. Выведет: Finished in 100 ms.
Я привел это лишь для того, чтобы пояснить принцип решения первоначального вопроса ("избавиться от async/await"). Понятно же, что в разных clause'ах паттерн матчинга на самом деле будет записана какая-то более сложная логика, от scatter-gather до каких-нибудь еще решений. Мне лишь нужно было показать элегантность message passing по сравнению с "костылями" типа state machine этих самых async/await.
Здравствуйте, SkyDance, Вы писали:
SD>Что, конечно, не умаляет достоинств команды .NET и конкретно C#. Правильной дорогой идут, добавляя лучшее из других языков, и делая очень работоспособные высокоуровневые обертки. Именно эти обертки, от Task.Run до WhenAny, и есть ответ на вопрос из сабжа. Беда с ними лишь в том, что иногда это таки нужно дебажить...
Это еще не самая большая беда. Без легковесных потоков — это просто набор костылей, которые по сути мало что меняют.
T>Это еще не самая большая беда. Без легковесных потоков — это просто набор костылей, которые по сути мало что меняют.
Так эти async/await и нужны именно для того, чтобы не писать рантайм по типу Эрланга.
Опять же, threadpool, если о нем думать как о scheduler'ах, и считать каждый Task отдельным "легковесным потоком", в общем-то, реализует данный сценарий. Просто инструментов для интроспекции там вообще нет, телеметрия ужасна, ну и в целом неэлегантно. Все-таки, Java уже скоро 30 (?) лет исполнится, а C# недалеко от нее ушел. В основном лишь в плане сахара.
Здравствуйте, mrTwister, Вы писали:
T>О да, есть Task.Run и ничего не надо переписывать. Ага, щаз! Потом продакшен внезапно перестает отвечать на любые запросы из-за thread pool depletion, так как скопилась очередь из Task.Run, которые выжрали все потоки из системного пула потоков и сервер встал колом
Ножом можно и убить, а можно помидор порезать. Разница по факту с твоим "go func(){result <- prime(10000)}()" только в дополнительной "церемонии" с каналами, а у SkyDance с процессами. Это не big deal в 90% задач, но абстракция async/аwait мошнее.
Здравствуйте, SkyDance, Вы писали:
SD>Мне лишь нужно было показать элегантность message passing по сравнению с "костылями" типа state machine этих самых async/await.
Это ты пока показом кодом не достиг, как мы видим, а мантрам "красиво, элегантно и очень легко дебажить" сложно возразить. Что до "молодцы в MS, содрали все хорошее с Erlang", то это просто чушь. Ни async/await(F#), ни PM(ML/Haskell) никакого отношения к Эрлангу не имеют.
Здравствуйте, novitk, Вы писали:
N>Ножом можно и убить, а можно помидор порезать.
Проблема в том, что через таски в C# убить можно случайно и незаметно в процессе порезания помидора. Например, можно сделать невинное свиду исправление, которое в какую-то синхронную функцию добавляет синхронный блокирующий вызов. Казалось бы, что такого: в синхронную функцию добавили синхронный выызов, что такого? Но потом оказывается, что это был первый блокирующий вызов в этой цепочке синхронных функций, которая инициируется из асинхронного контекста. И вуаля, асинхронный контекст заблокирован. И проверить это исправление на ревью или линтером очень сложно, потому что надо анализировать весь стек вызовов, а у нас все обмазано интерфейсами, абстрактными фабриками и поздним связыванием, и какой получится стек вызовов станет известно только в рантайме
N>Разница по факту с твоим "go func(){result <- prime(10000)}()" только в дополнительной "церемонии" возни с каналами, а у SkyDance с процессами. Это не big deal в 90% задач, но абстракция async/аwait мошнее.
Главная разница в том, что нет двух видов "цветов" функций. Нет никаких синхронных и асинхронных контекстов, которые надо постоянно учитывать.
Здравствуйте, SkyDance, Вы писали:
SD>Так эти async/await и нужны именно для того, чтобы не писать рантайм по типу Эрланга.
Ну да, костыли в C# — вынужденная мера.
SD>Опять же, threadpool, если о нем думать как о scheduler'ах, и считать каждый Task отдельным "легковесным потоком", в общем-то, реализует данный сценарий. Просто инструментов для интроспекции там вообще нет, телеметрия ужасна, ну и в целом неэлегантно. Все-таки, Java уже скоро 30 (?) лет исполнится, а C# недалеко от нее ушел. В основном лишь в плане сахара.
Я бы согласился, если бы нельзя было создать блокирующую Task'у. Но в C# это делается очень легко и незаметно. После этого абстракция scheduler'а начинает течь со страшной силой, так как он уже не управляет потоками. Эти потоки у него могут забрать в любой момент и не отдать, после чего он уже schedul'ить не сможет
Здравствуйте, mrTwister, Вы писали:
T>Проблема в том, что через таски в C# убить можно случайно и незаметно в процессе порезания помидора. Например, можно сделать невинное свиду исправление, которое в какую-то синхронную функцию добавляет синхронный блокирующий вызов. Казалось бы, что такого: в синхронную функцию добавили синхронный выызов, что такого? Но потом оказывается, что это был первый блокирующий вызов в этой цепочке синхронных функций, которая инициируется из асинхронного контекста. И вуаля, асинхронный контекст заблокирован. И проверить это исправление на ревью или линтером очень сложно, потому что надо анализировать весь стек вызовов, а у нас все обмазано интерфейсами, абстрактными фабриками и поздним связыванием, и какой получится стек вызовов станет известно только в рантайме
Согласен, бывает сплошь и рядом. Но ты так говоришь будто в GoLang это как-то решается. Оно решается?
T>Главная разница в том, что нет двух видов "цветов" функций. Нет никаких синхронных и асинхронных контекстов, которые надо постоянно учитывать.
Как же его нет, если ты или return пишешь или в каналы шлешь?
Здравствуйте, mrTwister, Вы писали:
T>Главное слово выделил, горутина — это внутренняя структура в go, которая не имеет никакого отношения к потокам. Это что-то типа класса Task в C#
D>>Программа на любом языке состоит из, как минимум, одного потока, который не блокирует современную многозагачную операционную сицтему.
T>Разница в том, что если, например в C# я запущу параллельно 1000 функций Thread.Sleep, то я создам и заблокирую 1000 потоков операционной системы, потому что Thread.Sleep синхронная. Если я запущу в go в горутинах 1000 функций time.Sleep, я не создам ни одного дополнительного потока операционной системы, потому что time.Sleep как и все остальные функции в go асинхронная. Аналогом time.Sleep из go является в .net Task.Delay, вот она асинхронная.
Как вызов функции foo() становится асинхронным? То, что потоки берутся из пула — это уже нюансы.
Task.Delay — и async/await — это про concurrency, который вовсе не обязательно parallelism. Вот и весь нюанс.
Простой вызов time.Sleep(1000), как и просто foo() в go блокирует текущую горутину, которая так, или иначе, выполняется на потоке (но тоток из заранее аллоцированного пула).
go time.Sleep(1000) — это асинхронно — создаём горутину и выполняем её на свободном потоке из пула, если нет, то ждём когда поток освободится. Это и есть отличие concurrency от parallelism.
Операционная система оперирует понятием "поток", а остальное — это навешанные абстракции. Чудес не бывает.
Здравствуйте, novitk, Вы писали:
N>Согласен, бывает сплошь и рядом. Но ты так говоришь будто в GoLang это как-то решается. Оно решается?
Да, в Go эта ситуация исключена, так как в Go все функции асинхронные и не могут блокировать потоки ОС. Даже если функция крутит бесконечный цикл, рантайм go ее в любой момент может остановить и передать поток OC другой горутине. Сам рантайм Go по умолчанию создает количество потоков ОС равное количеству ядер процессора, больше ему не надо.
T>>Главная разница в том, что нет двух видов "цветов" функций. Нет никаких синхронных и асинхронных контекстов, которые надо постоянно учитывать. N>Как же его нет, если ты или return пишешь или в каналы шлешь?
Сама же функция при этом не меняется, ей без разницы как с результатом поступит вызывающая сторона: проигнорирует, положит в канал, или начнет сразу использовать.
Здравствуйте, Doom100500, Вы писали:
D>Как вызов функции foo() становится асинхронным? То, что потоки берутся из пула — это уже нюансы.
Потому что он не блокирует поток ОС. При работе foo поток занимает, конечно, но foo не нужно дорабатывать до конца, чтобы отпустить этот поток.
D>Task.Delay — и async/await — это про concurrency, который вовсе не обязательно parallelism. Вот и весь нюанс.
Вот и любая функция в go (а так же горутины) — это тоже про concurrency, а не про параллелизм.
D>Простой вызов time.Sleep(1000), как и просто foo() в go блокирует текущую горутину, которая так, или иначе, выполняется на потоке (но тоток из заранее аллоцированного пула).
Горутину блокирует, а поток не блокирует. При вызове time.Sleep(1000) поток сразу отпускается и передается другой горутине. Тоже самое происходит при вызове Task.Delay в C#
D>go time.Sleep(1000) — это асинхронно — создаём горутину и выполняем её на свободном потоке из пула, если нет, то ждём когда поток освободится. Это и есть отличие concurrency от parallelism.
Горутина создается, но поток ОС она не потребляет, так как при вызове time.Sleep поток сразу отпускается.
D>Операционная система оперирует понятием "поток", а остальное — это навешанные абстракции. Чудес не бывает.
Ну да
Здравствуйте, mrTwister, Вы писали:
T>При вызове time.Sleep(1000) поток сразу отпускается и передается другой горутине. Тоже самое происходит при вызове Task.Delay в C#
Ок, дошло. Но почему вызов foo(), в которой обычные императивные вычисления — асинхронный? При условии, что там не используются ни sync, ни Sleep, ни каналы, ни select. Такой вызов и есть "по умолчанию".
Здравствуйте, Doom100500, Вы писали:
D>Здравствуйте, mrTwister, Вы писали:
T>>При вызове time.Sleep(1000) поток сразу отпускается и передается другой горутине. Тоже самое происходит при вызове Task.Delay в C#
D>Ок, дошло. Но почему вызов foo(), в которой обычные императивные вычисления — асинхронный? При условии, что там не используются ни sync, ни Sleep, ни каналы, ни select. Такой вызов и есть "по умолчанию".
Потому что компилятор принудительно в foo расставит точки останова, в которых будет отпускаться поток ОС. И даже если в foo крутится пустой бесконечный цикл, рантайм go через особые хаки такую точку останова туда внедрит.
Здравствуйте, SkyDance, Вы писали:
Аё>>Написать go вместо async и receive вместо await делает код легкочитабельным?
SD>await разве умеет pattern matching?
Какая связь?
Вообще мне го нравился его протоколами. Но дальше утилки для конечного пользователя у нас на продукте дело не пошло, к сожалению. Нет популярного web UI под го (и переписывать с ts нет смысла), а бек- что не заняла жава, дополнил питон и нод ts.
Здравствуйте, mrTwister, Вы писали:
T>Здравствуйте, Doom100500, Вы писали:
D>>Здравствуйте, mrTwister, Вы писали:
T>>>При вызове time.Sleep(1000) поток сразу отпускается и передается другой горутине. Тоже самое происходит при вызове Task.Delay в C#
D>>Ок, дошло. Но почему вызов foo(), в которой обычные императивные вычисления — асинхронный? При условии, что там не используются ни sync, ни Sleep, ни каналы, ни select. Такой вызов и есть "по умолчанию".
T>Потому что компилятор принудительно в foo расставит точки останова, в которых будет отпускаться поток ОС. И даже если в foo крутится пустой бесконечный цикл, рантайм go через особые хаки такую точку останова туда внедрит.
Ну так это не про вызовы, а вообще про все инструкции. Функция может и заинлайниться.
В любом случае, понятно почему всё async.
Здравствуйте, SkyDance, Вы писали:
SD>Опять же, threadpool, если о нем думать как о scheduler'ах, и считать каждый Task отдельным "легковесным потоком", в общем-то, реализует данный сценарий. Просто инструментов для интроспекции там вообще нет, телеметрия ужасна, ну и в целом неэлегантно. Все-таки, Java уже скоро 30 (?) лет исполнится, а C# недалеко от нее ушел. В основном лишь в плане сахара.
В java уже virtual threads есть. По-моему, самое удобное.
Просто пишешь многопоточный код, как обычно. И запускаешь сколько угодно тредов.
Ещё structured concurrency допиливают.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, mrTwister, Вы писали:
D>>Ок, дошло. Но почему вызов foo(), в которой обычные императивные вычисления — асинхронный? При условии, что там не используются ни sync, ни Sleep, ни каналы, ни select. Такой вызов и есть "по умолчанию". T>Потому что компилятор принудительно в foo расставит точки останова, в которых будет отпускаться поток ОС. И даже если в foo крутится пустой бесконечный цикл, рантайм go через особые хаки такую точку останова туда внедрит.
Не очень понял в чём разница. Это называется вытесняющая многозадачность. ОС сама отпускает потоки, даже если там бесконечные циклы крутятся.
Основная беда в C# что если ты даже обернул некий GetPrime в async, то это мало что даёт. Например, где-то в глубине этого GetPrime может быть какой-то код, который делает синхронные вызовы с блокировкой. Ну, например, ходит в сетевой кеш. И в итоге они блокируют ОС-поток, несмотря на твой async. И в этом случае придётся перекрашивать функции на всю глубину вызова, переписывать дохрена кода. Особенно весело, когда это библиотечный код, к которому нет доступа.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, ·, Вы писали:
T>>Потому что компилятор принудительно в foo расставит точки останова, в которых будет отпускаться поток ОС. И даже если в foo крутится пустой бесконечный цикл, рантайм go через особые хаки такую точку останова туда внедрит. ·>Не очень понял в чём разница. Это называется вытесняющая многозадачность. ОС сама отпускает потоки, даже если там бесконечные циклы крутятся.
async/await — это и есть по сути попытка реализации кооперативной многозадачности в рамках ограниченного количества потоков ОС. В go эта многозадачность реализована более грамотно и она там действительно не кооперативная, а вытесняющая
·>Основная беда в C# что если ты даже обернул некий GetPrime в async, то это мало что даёт. Например, где-то в глубине этого GetPrime может быть какой-то код, который делает синхронные вызовы с блокировкой. Ну, например, ходит в сетевой кеш. И в итоге они блокируют ОС-поток, несмотря на твой async. И в этом случае придётся перекрашивать функции на всю глубину вызова, переписывать дохрена кода. Особенно весело, когда это библиотечный код, к которому нет доступа.