Pull to refresh

Comments 53

Стоит сказать, что ваша идея уже реализована в java 21 - virtual threads.

не знал

Недореализована, так будет точнее. Вот если бы synchronized-блоки тоже были асинхронными...

Сейчас из-за блока synchronized не может виртуальный трид сняться с реального. Ну так уже давно пишут - не используйте synchronized а используйте другие примитивы. А во всем остальном (и по тестам) - это успех.

Только новый код-то можно писать и без virtual threads, те же suspend-функции из Котлина почти такие же удобные. На virtual threads облизывались рассчитывая с их помощью ускорить старый код... а там как раз synchronized на synchronized сидит и synchronized погоняет.

Ну например если код не старый, тот же tomcat - то выхлоп очень даже ничего. А synchronized - прокололись, но уже как я понимаю, уже думают как все под капотом починить.

Это вы про котлин, яву, но не про плюсы?

Вроде бы я где-то видел, что как раз в tomcat получилось не так хорошо из-за synchronized

synchronized-блоки это в java или под этим что-то подразумевается в плюсах?

Я про яву. Вроде как в с# зеленые потоки накрылись медным тазом. Ну может скопируют с явы во второй заход.

virtual threads это в java, поэтому было бы странно если бы плохо совместимые с ними synchronized-блоки были где-то в другом языке

Давно уже рассмотрены многочисленные варианты решения проблемы, идея "спрятать всё внутрь реализации std::thread" очевидна, но проблема в том, что нужно это на уровень кода протащить, неизвестно сколько памяти выделять на тред (стекфул корутина получается?) и так далее, в общем это очевидно, но не решает ничего.

Стекфул корутины тоже обладают немалыми проблемами, а основное преимущество это спрятать ход исполнения программы куда подальше, что как по мне не является преимуществом

Стеклес корутины да, добавляют новую сущность в язык, но впринципе эта сущность уже давно была - называется задача (операция). Просто раньше эта операция была разделена на 10 калбеков в разных функциях, а теперь будет представлена в явном виде в языке, плюс явно будет виден control flow с await / yield, это как по мне преимущество.

Никаких сломов парадигм стеклес корутины за собой не несут, это просто явный способ записывать то что уже было раньше, к тому же зачастую более эффективный. Насколько я понял вас только пугает "сложность", но очевидно кто то где то должен это реализовать, вы просто предлагаете чтобы эту сложность за вас реализовывала операционная система (за которую язык очевидно не может отвечать, учитывая что С++ используется и там где операционной системы нет)

Достаточно взять готовую библиотеку корутин, например https://github.com/kelbon/kelcoro, и внезапно окажется, что всё там просто и никаких "сложных вербозных" вещей писать не нужно

А почему стеклесс корутины в виде виртуальных тредов обязательно должны добавлять новую сущность в язык?

виртуальные треды это стекфул корутины вы так называете?

Нет, в моем понимании "виртаульные" треды могут и при помощи стеклесс корутин быть реализованы при правильной кодогенерации компилятора.

Почти ничего не понял из того что вы написали. Но я как раз не предлагаю чтобы эту "сложность" за нас решала ОС. Силами ОС ее решить не удастся (медленное переключение контекста, переход в kernel space), а именно решать компилятором. Не языковыми средствами, а именно правильной кодогенерацией на уровне компилятора.

компилятор он какого то языка компилятор, должны быть какие то языковые конструкции которые будут говорить компилятору как генерировать код

Так нет же, это не

какого то языка компилятор

это идея какого-то будущего компилятора:

Пишите синхронный код, он прост и понятен! А под капотом в будущем там может быть оказаться кооперативная многозадачность с асинхронщиной.

который берет любой код, как угодно написанный и волшебным образом заставляет его работать так, как хочется тому, кто применил этот компилятор к этому коду. Фактически это компилятор, который читает мысли того, кто его использует, компилятор-экстрасенс такой!

Ну супер идея же! Вы не находите? Компилятор-экстрасенс - это гораздо круче чем компилятор с искусственным интелектом, по моему.

А мне всегда было интересно, а почему силами ОС ее не решить? Тоесть, зачем в ОС все это медленное переключение контекста, переход в kernel space? Зачем нужен, например, переход в kernel space? Да, понятно, что это ради каких-то целей безопасности, но в корутинах у нас никакого kernel space нет, значит и здесь нам безопасность не нужна? Почему тогда не дать возможность отключить это в ядре? Ведь существует же движение постепенного переноса функций линукса из kernel space в user space. Может, и до потоков когда-нибудь дойдет

Чтобы вам в процесс не подсунули чужой поток со всем его стеком, например. На высоком уровне вы это сделать не можете, а на уровне API ОС - вполне. И остановит эту операцию только то, что ОС находится в защищённом режиме и сработает та самая защита

С корутинами такой проблемы просто нет, неоткуда взяться у вас чужой корутине

Тоесть, зачем в ОС все это медленное переключение контекста, переход в kernel space?

Вот вы про какое переключение контекста говорите? Если у нас есть 2 (например) потока на 2-х процесорной машине, то контекст переключать не надо, например, и там где было медленное переключение контекста на однопроцесорной машине появляется быстрое параллельное исполнение.

Вот можете тут посмотреть , я картинку нарисовал там которая поясняет эту мысль.

Ой, там ого-го много под капотом.... Даже если прикинуть что у каждого процесса есть свое адресное пространство, это уже должно навеять подозрения...

А задача ли это ЯП? Если мы хотим асинхронного I/O, то благоразумнее спрашивать с тех кто предоставляет нам этот I/O - ОС (overlapped), POSIX (AIO), перевод сокетов в асинхронный режим. А городить свои костыли для синхронных функций, мне кажется это архитектурно так себе затея.

Так там есть асинхронный I/O уже давным-давно - через колбеки. При его применении в лоб в прикладном коде получается лапша, проверено десятилетиями использования

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

Вот, допустим, мы в гринтреде и хотим прочитать массив байт из сокета. Системный вызов read так просто сделать нельзя, надо приостановить гринтред, добавить сокет в epoll, и вернуть управление планировщику. Библиотека для работы с сокетами обязана знать реализацию гринтредов, иначе она не сможет работать правильно. Каждая реализация гринтредов - это маленькая ОС, со своими системными библиотеками, несовместимыми с чем-то другим.

В то же время stackless корутины при адекватной реализации будут вполне совместимы друг с другим.

Ответ на несклько выше коментариев, которые говорят, что реализация "под капотом" это плохо.

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

Плюс, уже есть реальные реализации, которые показывают, что это возможно. И работает оно очень даже неплохо.

Также стоит указать на антипаттерн, когда "кишки наружу" - "заражение асинхронщиной", когда из- за вызова одной асинхронной функции в глубине кода требуется все синхронные функции сделать асинхронные.

не зависимо от типов тредов

как же это независимо от типов тредов будет оно переключать контекст? На каком уровне? Что будет если я захочу остановить функцию и когда случится какое то событие внутри моей программы разбудить? Всё сведётся к просто треду и обращению к системному планировщику, а это подразумевает что

  • система существует

  • система предоставляет какой то идеальный планировщик лучше которого мы сделать не можем

  • наш код каким то образом эффективно с ним взаимодействует

Жду реализацию на системном планировщике select по событиям внутри программы, будет какое то месиво из сондваров видимо

Также стоит указать на антипаттерн, когда "кишки наружу" - "заражение асинхронщиной", когда из- за вызова одной асинхронной функции в глубине кода требуется все синхронные функции сделать асинхронные.

да, верно подмечено. В некоторых задачах, где была теоретическая возможность обращения к чем-то внешнему типа сокетов, приходилось переводить все функции в async. И оверхед для процессора копится серьезный.

Так не должно возникать такой ситуации, когда где то внутри возникает необходимость сделать асинхронный операцию. Это говорит о непродуманности архитектуры.

Когда-то давно в Rust была идея сделать нечто похожее, но в итоге так и не получилось.

И слава Б-гу.

да, у меня тоже такая мысль возникала когда писал

Ну да. За время, потраченное на попытки понять, зачем корутины в таком виде в плюсах, можно изучить весь голанг и все способы, которыми его можно прокрутить к плюсовому коду и наоборот)

зачем корутины в таком виде в плюсах

Да они в таком виде почти во всех языках в плане внутренней реализации, только в других языках эта внутренняя реализация скрыта за стандартными классами и синтаксическим сахаром, а в плюсах, как обычно, все внутренности доступны наружу, сахара по-минимуму и куцая стандартная библиотека. Последствия разработки комитетом и старания соблюсти баланс между удобством разработки и don't pay for what you don't use c возможностями максимальной кастомизации (пока что для корутин баланс сильно перекошен по вторую сторону)

Вот и я о том же. Цель всего этого действа - сократить количество кода, улучшить понимание. То есть именно сахарок и важен. А с такой реализацией корутин проще по старинке всё на колбэках или прикрутить голанг сбоку ). Туда же идут и мертворождённые модули. Но зато галочку поставили, что есть. Зато рефлексия, которая нужна в нормальном виде ещё позапозавчера, доступна кое-как только через костыли. Видимо, будет, как с концептами: когда народ вволю намучался с SFINAE, наконец-то соизволили концепты подогнать. Я бы понял, если бы комитету надо было что-то изобретать постоянно, но ведь всё уже изобретено и обкатано...

Но ведь именно "сахарок" для корутин в язык и завезли в первую очередь да в полном объёме.

Да вот вообще не в полном. Исключительно в объеме необходимом и достаточном для написания библиотек, а вот так, чтобы обычному разработчику взять и начать писать корутины используя только стандартные средства языка и не испытывать при этом жгучую попаболь и разрывы мозга — увы, нет. В 23-м стандарте добавили std::generator, уже лучше, может к концу десятилетия появится какой-нибудь нормальный std::task...

Так ведь std::generator и гипотетический std::task - это не "сахар", а просто "изкоробочная" реализация.

Синтаксическим сахаром, просто по определению, может быть исключительно часть синтаксиса языка.

Источником нашей головной боли являются операции ввода-вывода. Это, как правило, медленные операции, протекающие намного медленнее скорости работы наших программ.

Да уж... А что же будет с вашей головой, когда вы дойдете до задач, в которых быстрые операции должны выполняться одновременно, если уже от медленных уже голова болит?

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

Типа мы пишем код как угодно, а под капотом у нас такая особенная магия, которая делает так, что наш код в любом случае работает асинхронно, как бы по-дурацки синхронно мы его не написали.

Давайте магия у нас теперь будет называться:

кооперативная многозадачность с асинхронщиной

Ага чтобы 1 процедура была на 47тысяч строк и сама асинхронно выполнялась частями.
И обязательно с использованием goto

Так и есть. Мне представляется что такой С++ компилятор компилировал бы всю программу в машинные коды, которые не использовали бы стек. То есть фактически все бы выполнялось бы, условно, в функции main(), и обязательно с использованием goto, без использования call/ret (стека), условно, все бы инлайнилось в main(). То есть stackless-корутины. Корутиновые фреймы эмулирующие стек выделялись в куче. Ну и этих точек входы было бы по числу ядер (например, 8 потоков). Отдельно стоял бы вопрос линковки бы с внешними функциями, других языков (скорее всего и проще запретить). Но вы это описали кратко емко.

Асинхронность (в т.ч. радикальная), event-driven programming, асинхронное взаимодействие процессов - как это визуализируются в схемах процессов, например, в нотации ЕРС? Что-то сложное принято представлять в картинках.

Надо просто перейти в системном плане к другой парадигме. Все по умолчанию должно выполняться асинхронно.

А вот если надо тогда пишешь ключевое слово - выполнять синхронно.

Асинхронность означает, что вы понятия не имеете в каком порядке будет исполняться код, когда вычисление начнётся и как долго придётся ждать результата. Плюс возникает новый класс ошибок типа dead lock.

Для python есть библиотека gevent, где пытаются скрыть асинхронность под капот. Вся радость заканчивается как только дело доходит до отладки.

когда вычисление начнётся и как долго придётся ждать результата

Вы и сейчас этого не знаете, потому что шедулер операционки может выдавать вашему процессу совершенно какое угодно количество времени.

Мы добавили новое ключевое слово co_no_await....

А "поверх" этой асинхронности взаимодействия с сетью накладываются ограничения... :-)
Если за N ms из сокета поступило менее X байт - закрой сокет и сделай X, если за M ms из сокета не поступило "полное" сообщение - закрой сокет и сделай Y, а вот если за N ms из сокета "ничего" не поступило - закрой сокет и сделай Z.... :-)

Что-то вы чрезмерно нагнетаете. Плюсы всегда были языком консервативным, всегда целились на минимизацию стоимости абстракций и don't pay for what you don't use, ну и еще в Комитете довольно сильное лобби разработчиков компиляторов, которые делают так, чтобы проще было им, а не тем, кто будет пользоваться их работой.

Но посмотрите, например, на C#. Там вся эта асинхронность спрятана под капот. Event loop там реализован в рантайме, менеджер потоков реализован в рантайме, огромная стандартная библиотека, многие методы которой имеют async-варианты, плюс хорошая поддержка всего этого на уровне отладчика. В итоге все как вы сказали, нередко перевод кода метода с синхронной на асинхронную парадигму может свестись по сути дела к простому добавлению ключевого слова await к вызовам в нем. Минимум головной боли для программиста, все уже существует и работает.

Ни дня без озеленения потоков!
(Последняя идея часто называется green threads)

В Go по сути реализовали то, что вы хотите: goroutines - это вполне себе полноценные userspace/green threads с полноценной помощью компилятора в их реализации.

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

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

Когда в версию Go 17 затащили preemptive scheduling горутин (если я не ошибаюсь, на основе кооперации вотчдога и сигналхендлера), они окончательно стали полноценными green threads.

User-space threads существовали ещё на рубеже тысячелетий. В разных инкарнациях: в рамках просто одного процесса, как M:N в системные потоки, может ещё какие-то варианты были.

Потеряли популярность они из-за того, что требовали экзотических фокусов для своей работы: если пользовательский код делал read(), то нельзя было просто дать ему дёрнуть syscall. Эту функцию надо было перехватить и подменить на вызов менеджера ввода-вывода, который оборачивал вызов в select(). И так с каждым потенциально блокирующим системным вызовом. И не забывать обновлять библиотеку каждый раз, когда что-то меняем в сисколлах.

Сделать всё это на уровне компилятора будет ещё сложнее, чем полагаться на системные библиотеки со стандартным API: придётся отслеживать все версии всех платформ где компилятор работает. А если для вашего языка есть несколько реализаций, то будет ещё веселее.

Sign up to leave a comment.

Articles