Как стать автором
Обновить

Комментарии 73

Парадигма async/awain (stackless корутины/процессы) - худшее, что придумано в мире асинхронного программирования. Ладно для Rust, который новый язык, ему не повредит. Наверное.

Но, например, в Python эта парадигма много вреда нанесла.

Чем async/await плохи? Тем, что блокируют возможность развития простого синхронного кода в сложный асинхронный методом переписывания ТОЛЬКО сетевой подсистемы.

Вот был, например, python с его библиотекой requests. Затем появился asyncio. Поскольку не было никакой возможности писать совместимый код (один появившийся async заставляет "промазывать" async'ами весь каскад всех вызовов, от низкого уровня до высокого), то в итоге на asyncio всё-всё-всё вместо переиспользования - переписывали.

А вот внедрение async/await в JS, например, прошло сравнительно безболезненно, поскольку до них использовали колбеки.

Что насчёт синхронного, но ходящего в сеть, кода для Rust? Он существует? Его много?

В Rust вы вольны использовать ту реализацию, которая больше нравится. Используйте акторы (https://github.com/actix/actix) или CSP (https://github.com/fereidani/kanal) или напишите что-то свое.

Что насчёт синхронного, но ходящего в сеть, кода для Rust? Он существует? Его много?

C10k на современном железе выглядит скорее как C100k или даже C1m. По крайней мере с обработкой 10 тысяч потоков справляется ноутбук, с которого я сейчас пишу. Конечно будут накладные расходы на переключения и async версия потребляет существенно меньше CPU.
Аналог requests в rust - reqwest имеет и синхронный и асинхронный апи, но асинхронный является основным.

В Rust вы вольны использовать ту реализацию, которая больше нравится.

это хорошо свободой, анархией, но плохо тем, что вредит переиспользованию кода.

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

Аналог requests в rust - reqwest имеет и синхронный и асинхронный апи, но асинхронный является основным.

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

В случае с async/await вам придётся переписывать все 100500 кода.

В случае со стекфул парадигмой (либо stackless, но доведённой до уровня компилятора) вам придётся переписать только сетевую часть вашей библиотеки (что может быть менее одного процента в этих 100500).

Я не критикую Rust, я критикую async/await подход, вернее даже конкретные его реализации.

Теоретически можно спустить это на уровень компилятора. Удалить ключевое слово async (вернее сделать чтобы оно не требовалось в декларации функции, а было бы аналогом spawn или go из golang), а await сделать возможным к появлению в любом месте. Тогда появится возможность исправлять синхронный код к асинхронному без переписывания его зависимостей, но, увы, для этого парадигма должна входить в сам язык.

В случае со стекфул парадигмой (либо stackless, но доведённой до уровня компилятора) вам придётся переписать только сетевую часть вашей библиотеки (что может быть менее одного процента в этих 100500).

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

Написать аналог планировщика, который будет раскидывать задачи можно и они есть, а разницы будет он вызываться как `await func`, `go func`, или `spawn func` не очень много. Посмотрите, например, на https://github.com/crossbeam-rs/, он позволяет писать код похожий на гошный

Скрытый текст
use crossbeam::channel;
use std::thread;
use std::time::Duration;

fn main() {
    let (sender, receiver) = channel::unbounded();

    for i in 0..5 {
        let s = sender.clone();
        thread::spawn(move || {
            thread::sleep(Duration::from_secs(1));
            s.send(i).expect("Failed to send data");
        });
    }

    for received in receiver.iter().take(5) {
        println!("Received: {}", received);
    }
}

Есть языки, где эти попытки были максимально продвинуты. Например Марк Леман внедрял в Perl библиотеку Coro. При этом он же имплементировал LWP::Coro (или Coro::LWP - давно это было, уже не помню точно) так, что 100500 наработок на LWP (а тогда на нём делали чуть ли не 100% парсеров интернета) просто начинали работать асинхронно, не подозревая об этом.

Получалось, что контекст переключается около сисколлов на чтение/ожидание сокета и это довольно хорошо.

Кроме того, если говорить о Golang, то у него горутины изначально. А потому понятие "писать синхронный код" для него тождественно "преодолевать трудности". Да есть mutex, но изначальная парадигма такова, что все по ней и идут.

JS, как я сказал выше, имел только колбечный набор накопленных библиотек, а колбеки всегда совместимы с любой формой асинхронщины, а потому JS подобных трудностей не испытывает: новые либы люди пишут на async/await, а старые на колбеках/промисах.

Увы, Rust, несмотря на то, что молодой язык, об этом не подумали и у них снова зоопарк. Поскольку из Rust условно говоря "легко достучаться до C", то и до сисколов так же легко достучаться, то есть легко наполучить кучу либ рассчитанных на однопоток + синхру. Жаль, что разработчики не проработали этот момент.

Есть языки, где эти попытки были максимально продвинуты. Например Марк Леман внедрял в Perl библиотеку Coro.

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

Кроме того, если говорить о Golang, то у него горутины изначально. А потому понятие "писать синхронный код" для него тождественно "преодолевать трудности". Да есть mutex, но изначальная парадигма такова, что все по ней и идут.

Потому что изначально есть рантайм с планировщиком горутин по процессам. И все равно нельзя приписать к функции go, превратив в горутину и она волшебно заработает без синхронизации с остальным кодом. Хотя да, в нем это делается значительно проще других языков.

Увы, Rust, несмотря на то, что молодой язык, об этом не подумали и у них снова зоопарк.

Скорее в первую очередь думали не об этом. Он проектировался с возможностью работать на голом железе без ОС как замена C и этом случае недоступна даже стандартная библиотека. А так если нужно есть async_std, но да он появился не сразу.

В Rust работают над решением проблемы цветных фукнций.

C10k на современном железе выглядит скорее как C100k или даже C1m. По крайней мере с обработкой 10 тысяч потоков справляется ноутбук, с которого я сейчас пишу.

Кстати, нет.

то есть C10k для 8 ядерного процесса становится C80k, но не более того.

является ли процессор гигагерцовым или десятигигагерцовым большого значения для C10k не играет.

проблема C10k произрастает из квантования процессорного времени внутри ядра, а потому слабо зависит от производительности современных процессоров по отношению к тем, когда была сформулирована C10k

Это только если потоки и правда потребляют свои кванты времени целиком, что не так в случае когда они IO Bound.

так когда их 10к+ то и получается почти полная утилизация каждого кванта

А это от нагрузки зависит.

Вот да. Именно об этой проблеме я говорю! Спасибо за мем!

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

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

И если язык успел просуществовать без этих самых обёрток хоть какое-то время - в его экосистеме неминуемо расплодились программы и библиотеки, которые активно используют "голые" системные вызовы через ffi.

Кстати, ffi тоже нужно проектировать в гипотетическом языке с M:N параллелизмом с осторожностью, поскольку внешний мир про этот самый параллелизм-то не знает. В частности, большинство языков, включая Python, были "обречены" на async/await именно из-за слишком свободного ffi и необходимости сохранять обратную совместимость с нативными модулями.

Потому что в противном случае любая библиотека, случайно либо намеренно обошедшая эту самую сетевую подсистему, ломает всё.

да, но нет.

смотрите, например у нас есть большой пул роботов, качающих что-то из интернета и складывающих результаты работы в БД. Переписать придётся хождение в интернет и хождение в БД.

Как правило, большинство вопросов "как скачать страничку из интернет" или "как сделать запрос к БД" для языка X дают 1 mainflow которым будет пользоваться большинство.

А когда появится второй flow, в этот момент будет стоять вопрос совместимости с mainflow. Так вот когда во втором flow появляется async/await приходится переписывать ВСЁ.

Только в одном языке появление второго flow прошло сравнительно успешно - JS, потому что первый flow был на колбеках, а колбеки совместимы с ЛЮБЫМ асинхронным движком

Лучше переписанное всё, чем нерабочее и глючное всё.

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

сравните:

  • переписать сетевую библиотеку

  • переписать ВСЁ (картинка ниже), включая парсеры и проч

Но у вас нет опции "переписать сетевую библиотеку", потому что у вас, блин, в языке может вообще не быть сетевой библиотеки! И даже если есть - нет никакой гарантии, что другие библиотеки пользуются именно ей!

Возьмём для примера ваш любимый Python. Для работы с сетью там предлагались минимум три библиотеки - языковая, биндин к openssl, и биндинг к curl. От переписывания первой с учётом гринтредов у вас ни openssl, ни curl автоматически не заработают!

А в системных языках (Си, С++, Rust) всё ещё хуже, потому что там этих "сетевых библиотек" бесконечно много.

Но у вас нет опции "переписать сетевую библиотеку", потому что у вас, блин, в языке может вообще не быть сетевой библиотеки!

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

обычно библиотеки ходящие в сеть не представлены в большом множестве.

От переписывания первой с учётом гринтредов у вас ни openssl, ни curl автоматически не заработают!

это почему это?

А в системных языках (Си, С++, Rust) всё ещё хуже, потому что там этих "сетевых библиотек" бесконечно много.

в Си и С++ никогда не было единого репа "где взять либу". Там всегда бардак был на эту тему: иди по сайтам копайся, ищи.

в Rust есть. И поиском "как запаковать json/yaml" или "как установить TCP-соединение" человек придёт к 1-2 вариантам.

это почему это?

Потому что они не используют языковую сетевую библиотеку, очевидно же.

ну дык взять ту, которые используют. вынести openssl в отдельный тред, на худой конец.

  • в чём задача асинхронности?
    - в том, чтобы утилизировать CPU на 100%.

(я когда-то на HighLoad читал доклад об этом, надо б найти).

  • зачем это делать? почему бы не запустить столько тредов, сколько нужно задач?
    - Потому что планировщик операционной системы выполняется на фиксированной частоте (Linux до версии 2 - 100Гц, Linux после версии 2 - 1000Гц) и когда количество тредов (процессов) становится (больше или даже соизмеримо!) этой частоты (умноженной на число ядер), то такое планирование процессов приводит к радикальному недоиспользованию CPU. Выход - только оставаться в том же процессе, продолжая выполнение задачи.

Как-то так. То, что openssl будет есть CPU того же треда, где asyn-функции выполняются - не так и плохо.

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

ну дык взять ту, которые используют. вынести openssl в отдельный тред, на худой конец.

Вы api openssl видели? Вот вызвали вы функцию SSL_read, та вызвала системный вызов read и ждёт пока в сокете окажутся данные. Как ваша сетевая библиотека поможет вам нагрузить CPU на 100% в этой ситуации? И если вы вынесли этот вызов в отдельный поток - чем такое решение отличается от обычного многопоточного сервера и причём тут вообще гринтреды?

в API openssl я смотрел мельком, зато я видел 100500 асинхронных приложений, умеющих в SSL. то есть в них как-то этот вопрос решили. Даже если бы это было не решено, то в любом случае, это переписывание одной (прописью - одной) библиотеки

Разумеется, этот вопрос решаем. Однако, вы утверждали, что достаточно переписать одну-единственную сетевую библиотеку, чтобы всё автоматически заработало. А это не так.

одну — две, какая разница по сравнению со 100500?

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

речь идёт о переписывании синхронного кода в асинхронный. в синхронном коде обычно нет такого понятия, как "примитивы синхронизации". Они добавятся как раз при этом переписывании и внутрь библиотеки (библиотек) которые ранее выполняли сетевое взаимодействие. Как правило это всего два-три места: хождение в интернет + хождение в 1-2 БД

в синхронном коде обычно нет такого понятия, как "примитивы синхронизации"

А это тогда что?

https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject
https://en.cppreference.com/w/cpp/thread/mutex
https://doc.rust-lang.org/std/sync/struct.Mutex.html

а это уже мультитредовый код.

такое ОЧЕНЬ редко встречается на продах. чаще всего множество single-thread приложений, каждое из которых синхронное или асинхронное.

У кого редко, а у кого вообще все программы многопоточные.

многопоточные программы (в смысле процессов или тредов, а не асинхронных процессов) как правило (не в 100% случаев, но как правило) признак некомпетентности разработчика. У многопотока крайне узкая ниша, вроде выноса вычислений в другой поток итп

То что многопоточные сервера неэффективны, все выяснили примерно в 200x - именно тогда пошёл бум ухода от многопотоков к однопотокам с асинхронной парадигмой, навроде nginx

Во-первых, сам nginx многопоточный. Не как сервер, а как приложение.

Во-вторых, одними серверами программы не ограничиваются, кому-то надо и десктопные приложения писать. И вычисления тоже, представляете себе, кто-то должен делать!

В-третьих, вы ставите телегу впереди лошади. Мы же сейчас обсуждаем старые языки, в которых удобной асинхронности изначально не было. Поэтому аргумент "потоки не нужны, потому что есть асинхронность" не катит.

И вычисления тоже, представляете себе, кто-то должен делать!

я ниже описал.

вычислениям не нужна асинхронность.

но ещё раз: цель внедрения асинхронности - максимально полная утилизация CPU.

Если вы её утилизируете без всяких ухищрений на 100% простыми вычислениями, то вам асинхронность скорее всего навредит или не нужна.

вообще возня с асинхронностью растёт из-за несовершенства системного планировщика процессов.

довольно несложно представить себе некий мир, в котором перепишут системный планировщик процессов и нахрен станут не нужными все эти asyncio, Coro, libev, libuv и ппрочие

Эта задача, кстати, вполне реализуема - нужна команда, которая возьмётся за эту задачу. Но на фоне массовых увольнений из разработчиков ядра по национальному признаку, такое нам не светит. Увы.

довольно несложно представить себе некий мир, в котором перепишут системный планировщик процессов и нахрен станут не нужными все эти asyncio, Coro, libev, libuv и ппрочие

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

И подобное "переписывание" приведёт всё к той же проблеме "двуцветных" функций...

Только вот проблема-то не в планировщике процессов, он как раз справляется хорошо

как это, хорошо?

смотрите, возьмём Linux. Частота переключения процессов не может быть выше, нежели 1000Гц. То есть квант времени равен 1мс.

допустим Вашей программе нужно подождать 0.1 мс. Она засыпает на сокете, но проснётся не ранее чем чере 1 мс.

это называется "хорошо работает", да?

Если Вашей программе нужно подождать 1.1 мс, то она проснётся через 2мс.

именно отсюда растёт проблема C10k.

как её решали?

стали составлять список задач "требуется подождать", и поскольку "подождать" требуется в основном только на сокетах, то именно к ним и приделали select, а потом epoll.

Дальше каждая программа, которой требуется преодолеть C10k, собирает в один epoll всё множество своих "ожидающих", а затем укладывается "спать" на время соответствующее наименьшему ожиданию (а иногда получается и вовсе не спит).

если epoll всё же спит, то спит вот так, как описано выше - кратно 1мс. В этом случае, утилизация CPU на 100% не получается. Но такие события происходят либо сравнительно редко (что пофиг) либо на ненагруженном ПО (что снова пофиг)

как-то так

смотрите, возьмём Linux. Частота переключения процессов не может быть выше, нежели 1000Гц. То есть квант времени равен 1мс.

Это верно только для CPU-bound потоков, IO-bound потоки переключаются как только блокируются на вводе-выводе.

допустим Вашей программе нужно подождать 0.1 мс. Она засыпает на сокете, но проснётся не ранее чем чере 1 мс.

Если вам так важно проснуться строго через 0.1 мс в задаче ввода-вывода - то вам нужна ОС реального времени. Никакая сетевая библиотека тут не поможет в принципе. Однако, для задач тайм-аутов ввода-вывода точности в 1мс более чем достаточно.

Не забывайте, что через эту 1мс проснётся не один поток, а все потоки.

Дальше каждая программа, которой требуется преодолеть C10k, собирает в один epoll всё множество своих "ожидающих", а затем укладывается "спать" на время соответствующее наименьшему ожиданию

…и точно так же спит 1 мс вместо запрошенной 0,1мс. После чего возобновляются, опять-таки, все ожидающие задачи - прямо как в ситуации с потоками.

именно отсюда растёт проблема C10k.

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

Вся суть select/epoll/IOCP - в уменьшении числа переключений контекста, а вовсе не в обработке таймеров.

вычислениям не нужна асинхронность.

Однако, вычислениям нужны потоки. И эти потоки как-то должны сосуществовать в языке вместо с асинхронностью одновременно.

в этом месте мы приходим к парадигме вроде golang - вытесняющая многозадачность в userspace.

если вам нужен процессор на очень долго, то runtime языка сможет его прерывать, прокручивая evloop прочих асинхронных процессов. При этом можно ограничить квант времени "сверху" (непрерывно выполняется не дольше...), но не вводить ограничение снизу.

Но в этом случае всё равно рантайм получается отличным от системного и вот это место и является тем из-за чего Go сложно сопрягать с Сишными либами.

Для языков, которые изначально не были заточены на универсальность (ОДНОВРЕМЕННУЮ поддержку И асинхронных приблуд, И долговычислительных приблуд), можно это всё оставлять на совести пользователя: долговычислительные приблуды он может выносить в треды - в них утилизация CPU на высоких уровнях будет получаться сама собой.

как-то так

И так, вы признали что многопоточность в языке если не нужна, то возможна? А если есть многопоточность. то и примитивы синхронизации тоже нужны? Теперь предлагаю вспомнить с чего началось. А началась ветка с моего утверждения о том, что сетевую библиотеку переписать недостаточно, надо переписать ещё и механизмы синхронизации.

Теперь вы согласны с этим утверждением?

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

а с моей точки зрения дискуссия началась о том, что async/await - худший паттерн из асинхронных (ниже есть даже картинка), поскольку ЗАСТАВЛЯЕТ при переходе с синхронного кода к асинхронному переписывать огромное количество кода, которое могло оставаться таким же, будь паттерн другим.

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

Конкретно эта ветка началась именно с примитивов синхронизации. Если вы сами ответили тремя комментариями на один, начав тем самым три разные ветки обсуждения - будьте добры, оставайтесь в рамках темы этих веток.

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

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

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

Опять двадцать пять. Ну не можете вы в существующем языке просто взять и объявить, что отныне многопоточные приложения не поддерживаются, потому что их, видите ли, не большинство! Если вы берётесь менять часть стандартной библиотеки языка - вы обязаны поддерживать все уже существующие программы, или хотя бы предоставить инструкции по миграции.

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

что значит "не поддерживаются"? я такого нифига не говорил.

многопоточные приложения остаются там же где и были.

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

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

Если вы берётесь менять часть стандартной библиотеки языка - вы обязаны поддерживать все уже существующие программы

обсуждаемый Python временами плевал на этот принцип, кстати.

Что произойдёт, если код, выполняющийся в другом потоке, попытается сделать запрос в сеть?

обсуждаемый Python временами плевал на этот принцип, кстати.

и это не то, чем ему следует гордиться

Что произойдёт, если код, выполняющийся в другом потоке, попытается сделать запрос в сеть?

асинхронная машина должна быть запущена в каждом треде (сам язык может это обеспечить) и нерешаемых проблем с этим не выглядит что есть

и это не то, чем ему следует гордиться

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

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

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

именно чтоб полагаться на это

именно чтоб код однопоточного приложения почти полностью совпадал с кодом многопоточного

В частности, большинство языков, включая Python, были "обречены" на async/await именно из-за слишком свободного ffi и необходимости сохранять обратную совместимость с нативными модулями.

ЕМНИП в Python была (есть?) реализация гринтредов, без ключевых слов async/await.

Просто почему-то решили на борт, в stdlib взять именно async/await, а не эту машину.

Эта реализация гринтредов была в отдельном интерпретаторе, который Stackless Python. Интерпретатор невозможно просто взять и "затащить" в стандартную библиотеку.

но на переходе к версии 3 от версии 2 они могли это сделать. async/await ЕМНИП появился именно в версии 3, причём не в 3.0

Переход от версии 2 к версии 3 был не настолько глобальным, там поменялся, главным образом, парсер. Странно говорить это про изменение, расколовшее экосистему надвое, но это очень простое изменение.

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

Переход на гринтреды куда более всеобъемлющ, и требует изменения API нативных модулей.

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

Вот смотрите, вы вызвали некоторый нативный модуль. Этот нативный модуль вызвал колбек из вашего кода. Этот колбек пошёл в сеть.

Что должно произойти? Переключение гринтреда. Но на стеке-то - нативные фреймы, от модуля который ни про какие гринтреды не знает. Всё, приплыли.

давайте по порядку.

как выпрямляют асинхронный код? Вводят какой-то оператор "переключения контекста" и вызывают его вручную. Оператор иногда зовут yield иногда cede, но смысл одинаковый.

Теперь вопрос "где вызывать этот оператор?"

Можно конечно дать обобщённую рекомендацию вида "оператор вызывать каждые X итераций CPU". Скажем у нас цикл - каждые 100 итераций по циклу.

А на практике хорошо работает следующая хрень: оператор переключения контекста вызывается ТОЛЬКО у функций, работающих с сетью (и межасинхронными каналами).

То есть контекст переключается при вызове ОЖИДАЮЩИХ функций (ожидаем данных из сокета, канала, от пользователя? - можем переключить контекст).

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

Идеальный асинхронный код имеет маркировку асинхронности только в момент его запуска. `go function` - для Golang, или `async sub` для Perl.

Отвечая на ваш вопрос. Все функции "колбек пошёл в сеть" пойдут в эту сеть через стандартную (сравнительно стандартную) библиотеку. Внутри которой может быть оформлен вот этот самый вызов cede, yield итп

Переписать только её и только в области хождения в сеть.

PS: хождение на диск, хоть и похоже на хождение в сеть, но обычно их асинхронностью не парятся - не даёт значимого профита, кроме геморроя.

Теперь вопрос "где вызывать этот оператор?"

Нет, вопрос в другом. Вопрос - "как этот оператор должен работать?".

И фундаментальная проблема, которую вы никак не углядите, тут в том, что этот оператор должен работать с кодом, который:

  1. написан на другом языке,

  2. написан задолго до того, как вы свою реализацию придумали;

  3. был скомпилирован задолго до того, как вы свою реализацию придумали;

Отвечая на ваш вопрос. Все функции "колбек пошёл в сеть" пойдут в эту сеть через стандартную (сравнительно стандартную) библиотеку. Внутри которой может быть оформлен вот этот самый вызов cede, yield итп

Ага, а дальше-то что? Вот вызвала ваша библиотека yield, гринтред переключился (как?!), потом пришёл ответ, гринтред переключился обратно, управление вернулось в колбек, из колбека управление вернулось в нативный модуль, нативный модуль упал.

Вы осознаёте, что от языка люди ждут что программы на нём будут работать, а не падать? Особенно если эти программы уже работали в прошлом?

Нет, вопрос в другом. Вопрос - "как этот оператор должен работать?".

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

И фундаментальная проблема, которую вы никак не углядите, тут в том, что

перечисленное не такие уж и проблемы. Вернее проблемы, но решаемые переписыванием библиотек, работающих около сети и только.

написан на другом языке.

и ходит в сеть. такой случай крайне редок. Решается переписыванием этого фрагмента в другую парадигму.

задача условно-сложная, но и достаточно редкая

написан задолго до того, как вы свою реализацию придумали;

это вообще не проблема. ещё раз аппелирую к именно такому случаю и Марку Леману. Он (он - автор libev и libcoro, на которой построено чуть ни более чем половина асинхронных приложений интернета) взял и для популярной библиотеки написал враппер. И 99.9% написанных на тот момент программ стало возможным запускать на асинхронной парадигме

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

если у нас closed source, то в этом случае его можно вынести в thread pool и оттуда запускать с лимитами по числу выделенных процессов.

очевидно, что ничего больше не сделать.

таким способом, кстати делают и неблокирующий fio, поскольку система тупо его поддерживает (поддерживала?) только в блокирующем режиме

Ага, а дальше-то что? Вот вызвала ваша библиотека yield, гринтред переключился (как?!), потом пришёл ответ, гринтред переключился обратно, управление вернулось в колбек, из колбека управление вернулось в нативный модуль, нативный модуль упал

я похоже не понимаю тот пример, что вы приводите. почему нативный-то упал?

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

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

Надёжнее всего работают волокна (те же потоки с полноценным стеком, только с реализацией полностью в пространстве пользователя). Но и они ломаются если переключать контекст в колбеке.

перечисленное не такие уж и проблемы. Вернее проблемы, но решаемые переписыванием библиотек, работающих около сети и только.

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

Напомню мой основной тезис: в новом языке программирования можно навертеть всё что угодно, а вот в старом без переписывания не обойтись. И явное указание цвета функции тут - не проблема, а благо, потому что избавляет от ошибок.

я похоже не понимаю тот пример, что вы приводите. почему нативный-то упал?

Потому что ему насильно переключали контекст, на что он не рассчитан.

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

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

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

Погодите. Они все работают с такими модулями. Просто пока из модуля не вернётся управление, оно не вернётся.

А также любых библиотек, которые могут вызывать колбеки. Что, применительно к Питону, означает "любых библиотек".

я не понимаю про колбеки, извините. Я выше попросил переформулировать Ваш пример

Напомню мой основной тезис: в новом языке программирования можно навертеть всё что угодно, а вот в старом без переписывания не обойтись.

напомню мой тезис: в старом языке программирования возможно сделать так, чтобы большинство кода не требовало быть переписанным. Пример перед глазами: Марк Леман и его библиотеки libev и libcoro всунутые в Perl.

В питон вполне тоже можно было всунуть libcoro и libev. В этом случае, ВЕСЬ КОД использующий requests не потребовалось бы переписывать. Ни строчки.

С учётом того, что python сотворил с переходом от версии 2 к 3, он мог себе позволить стековые асинхронные процессы.

Кроме того, можно было взять бесстековые но на уровень VM и снова дать возможность делать совместимый старый-новый код.

Ломаются любое паттерны программирования, полагающиеся на тот факт, что входы в функции и выходы из неё являются правильными скобочными последовательностями (в пределах одного потока).

В пределах одного асинхронного процесса этот факт не ломается ни при asyncio ни при greenthread

Условно, код может писать что-то в глобальную (или thread static) переменную, а при выходе - восстанавливать старое значение.

условно-опасный код возможен. эта проблема существует, но она существует и для async/await кода и для файберов. И даже для колбечного кода с промисами.

язык не имеет значения, Rust тут не исключение.

Погодите. Они все работают с такими модулями. Просто пока из модуля не вернётся управление, оно не вернётся.

Колбеки.

я не понимаю про колбеки, извините. Я выше попросил переформулировать Ваш пример

Что именно не понятно-то, блин?

Кроме того, можно было взять бесстековые но на уровень VM и снова дать возможность делать совместимый старый-новый код.

С сохранением совместимости API нативных модулей - нельзя. Просто сравните как выглядит этот API в Питоне и, скажем, в Lua, где интерпретатор и правда поддерживает гринтреды.

В пределах одного асинхронного процесса этот факт не ломается ни при asyncio ни при greenthread

Ломается, и с треском. Реальность не обязана подчиняться вашим желаниям, и от того, что вы ещё 10 раз скажите, что оно не ломается - сломанный код не заработает.

условно-опасный код возможен. эта проблема существует, но она существует и для async/await кода и для файберов. И даже для колбечного кода с промисами.

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

Колбеки

неа, не понимаю

из НЕсетевых примеров кода с колбеками на ум приходит только алгоритм сортировки, увы

приведите пример

С сохранением совместимости API нативных модулей - нельзя. Просто сравните как выглядит этот API в Питоне и, скажем, в Lua, где интерпретатор и правда поддерживает гринтреды.

что такое бесстековая корутина?

это ПО ФАКТУ препроцессор, разворачивающий псевдо-синхронный код в промисы с колбеками

если оформлять как промис ВСЕ функции (например), то ключевые слова async/await можно опустить

Ломается, и с треском.

серебряную пулю получить невозможно

даже если Вы приведёте таки несетевой пример, то мы посчитаем насколько часто он может встречаться в клиентском коде

приведите пример

В Питоне на любой чих можно сделать "магический" метод. Эти методы могут вызываться в том числе и из нативных модулей.

если оформлять как промис ВСЕ функции (например), то ключевые слова async/await можно опустить

Нативные тоже оформлять будете? Если нет, то страдают колбеки. Если да - то страдает API. В любом случае ломается обратная совместимость.

серебряную пулю получить невозможно

А вы почему-то пытаетесь.

Нативные тоже оформлять будете

компилятор или интерпретатор может оформить все функции как с промисами

А вы почему-то пытаетесь.

я указываю, что есть две дороги

  1. с количеством страданий Х

  2. с количеством страданий Х/10

Пример:

# Нативная библиотека, реализация ниже
from nativelib import magic, run_with_cookie
# Здесь подконтрольный код, его можно как угодно преобразовать
# Как угодно
s1, s2, s3, s4, s5 = ... # сокеты

# Вызываем нативную функцию из питона
magic(42, lambda: s1.read())
magic(10, lambda: s2.read())
# "горутина" со чтением
go:
  s3.read()
  run_with_cookie(12, lambda: s4.read())

# Заблокируем главный поток на мютексе
run_with_cookie(0, lambda: s5.read())
//! Реализация нативной библиотеки на псевдокоде
//! Изменить эту библиотеку нельзя, потому что:
//! 1) Она реализована на другом языке
//! 2) Она скомпилирована в бинарник и выложена на pypi



pub fn magic(cookie, cb) {
  thread::spawn(|| {
     run_with_cookie(cookie, cb);
  });
  return;
}

pub fn run_cookie(cookie, cb) {
  // Обычный системный мютекс, блокирует системные потоки
  // Про горутины ничего не знает
  global_mutex.lock();
  global_var = cookie;
  cb();
  assert!(global_var == cookie);
  global_mutex.unlock();
}

Вопрос в том, как это будет исполняться? Допустим, что у нас всего один реальный поток в интерпретаторе.

В частности, что произойдёт, когда cb() наткнётся на s1.read() - по Вашим словам, он должен сделать yield. Это, в свою очередь, остановит исполнение потока и запустит на этом же потоке другую горутину. Что будет, если другая горутина - это go с s3, s4? Как они себя будут вести?

Если я правильно понял, то давайте рассмотрим сперва только первый листинг.

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

именно в этом месте видны проблемы async/await. С ними вы этот код as is в асинхронный не переделаете.

придётся обмазывать все вызовы операторами await.

но пойдём дальше.

s1.read() заблокирует файбер в котором он запущен.

если на этот момент существует s2.read() и ожидает данных из сокета и эти данные пришли, то управление будет пердано ему.

В данном случае подобное поведение для кода штатное ибо из Rust вы потоки запускаете, то есть предполагается, что s1.read() может завершиться как раньше так и позже s2.read()

В общем не вижу проблем в преобразовании данного кода в асинхронный

придётся адаптировать socket.read() для работы в асинхронной среде, и если применена унылая async/await парадигма обмазать await'ами все эти самые read'ы.

теперь рассмотрим второй листинг

там мы видим thread::spawn, очевидно, что при переходе на асинхронную парадигму от тредов нужно избавляться в пользу файберов: это и улучшит быстродействие и уменьшит количество потребляемых ресурсов. Соответственно этот код придётся порефакторить ВНЕ ЗАВИСИМОСТИ от того на каком языке он написан.

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

magic запускает лямбду в том, что вы назвали "горутиной"

Нет, magic выполняет лямбду в самом обычном потоке ОС, который создан через Раст/Си.

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

Также я предполагаю, что в питоне передали всё на стеквфулл корутины, как Вы и предлагаете. Передали также сетевую библиотеку, чтобы она делала yield. И сделали так, что код по умолчанию исполняется в некой «горутине». Итого никаких async/await здесь нет и не должно быть.

Если сделать итог по используемым потокам, то у нас здесь есть 3 потока ОС:

  1. Поток интерпретатора, на котором должны крутится все горутины

  2. Два потока, которые спаунятся в расте.

В общем не вижу проблем в преобразовании данного кода в асинхронный

Меня в этом коде не интересует «преобразование его» в асинхронный. Меня интересует, что будет в этом коде происходить с Вашей моделью стекфул тасок/горутин, которую Вы предлагаете. Предположим она уже сделана и сетевая библиотека переписана.

если на этот момент существует s2.read() и ожидает данных из сокета и эти данные пришли, то управление будет пердано ему.

Сокета s2 не может быть в этот момент из-за мбтекса. В этом примере, в принципе, важно обратить внимание на то, что происходит в нативной части.

там мы видим thread::spawn, очевидно, что при переходе

Нет, по двум причинам сразу. Первая - как писал выше, нет задачи переделать на асинхронную парадигму. Вторая - нативного кода, который спаунит потоки ОС уже написано довольно много. Вы предлагаете переписать весь нативный код?

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

Сокета s2 не может быть в этот момент из-за мбтекса.

Так и в чём проблема? значит никакой файбер в планировщике файберов не зарегистрирован, как ожидающий пробуждения.

код инициализации s2 тоже выполняется "сверху вниз"

я пока так и не понял проблему, что Вы пытаетесь описать

ВСЁ что работает с async/await может работать и с файберами. Новых проблем не появится.

Нет, по двум причинам сразу. Первая - как писал выше, нет задачи переделать на асинхронную парадигму.

тогда я не понимаю какая задача есть.

я говорил о проблеме: имеется 100500 синхронного кода. Если мы хотим переиспользовать этот код по максимуму as is, то парадигма async/await заставляет нас переписывать почти всё, а парадигма файберов позволит переписать только (около)сетевую часть.

Если Вы этому не оппонируете, то я потерял нить обсуждения.

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

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

если вы запрещаете переписывать вообще что либо, то ваш код остаётся с вами, новых вещей у вас не появляется.

как-то так

Так и в чём проблема? значит никакой файбер в планировщике файберов не зарегистрирован, как ожидающий пробуждения.

Проблема в том, что вызов cb(); "проснётся" не в том потоке, в котором засыпал, и global_mutex.unlock(); вылетит с ошибкой. Это первый сценарий.

Во втором сценарии из-за переключений вокруг s4 и s5 вызов global_mutex.lock(); будет выполнен два раза подряд в главном потоке, что приведёт либо к взаимоблокировке, либо к перезаписи global_var и падению на строке assert!(global_var == cookie);

Проблема в том, что вызов cb();

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

Давайте вообще подумаем, где этот асинхронный код может быть нужен.

прежде всего - сетевое взаимодействие. 99% асинк приложений - это работа с сетью.

если Ваш код с сетью не работает, то в 99% случаев асинхронщина Вам не нужна.

в оставшемся 1% значительным пулом выделяются GUI-приложения. Там, увы, много-много лет принято писать "в колбеках". Опять же Марк Леман пытался притащить в GUI-мир асинхронный фреймворк (и даже целый эмулятор терминалов для этого написал - rxvt) но в целом мир продолжает жить на колбеках

как-то так

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

Вся разница в том, что при подходе с горутинами/гринтредами/волокнами красить и переписывать приходится внеязыковые библиотеки, а при подходе async/await - внутри языковые.

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

я считаю, что async/await популярен поскольку путь к нему лежит через ПОНЯТНЫЕ (привычные) промисы.

fiber/greenthread - это условно полноценный планировщик, а потому дорога к нему несколько сложнее.

только в этом причина популярности async/await.

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

именно поэтому у вас всё время рождались примеры именно с внешним языком.

ибо в случае с тем же языком, без async/await вообще всё идеально :)

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

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

именно поэтому у вас всё время рождались примеры именно с внешним языком. ибо в случае с тем же языком, без async/await вообще всё идеально :)

Ну так я сразу же написал, что ffi - одна из основных проблем, мешающих ввести в язык гринтреды прозрачно.

Просто в контексте конкретно Питона наличие нативных модулей - это данность, язык с самого начала задумывался как "клей" для нативного кода.

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

принципиально никакая асинхронная программа не отличается от другой асинхронной программы

принципиально все они одинаковые

речь о семантике.

семантически асинхронная программа может быть написана

  • в колбеках

  • в промисах

  • в async/await-парадигме (по факту препроцессор над промисами)

  • в fiber/greenthread-парадигме

максимально совместима с простым синхронным кодом только последняя парадигма

100% совместимости с синхронным кодом не имеет никакой из вариантов.

ибо даже если Вы введёте колбеки, то вам придётся что-то отрефакторить, чтобы появилось место, их вызывающее.

Я не понимаю что вы пытаетесь сказать. Какое отношение написанный вами комментарий имеет к процитированному сообщению?

ладно, проехали, вероятно, надо заканчивать эту пустую дискуссию

Зарегистрируйтесь на Хабре, чтобы оставить комментарий