Pull to refresh
3
0
Ильмир Усманов @ilmirus

User

Send message

Ну вот, а то "три месяца", "три месяца". Годы же.


3 года и 8 лет — это величины одного порядка. Можно считать равны для проектов типа языков программирования.

По крайней мере в моей голове.

Если то, что происходит в мире не соответствует тому, что происходит в вашей голове, это проблема мира, ага.


Так вот проекты идут и 3 и более лет в моей жизни тоже.

Это мало. Языки программирования разрабатывабтся десятилетиями.


Ни одна фича не может занимать сама по себе более 3 месяцев.

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


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

Примеры будут? Обратный пример — добавление джененериков в ту же Джаву. Тоже был многолетний труд.

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

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


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


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


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


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

У вас в опыте не было фич, которые занимают годы, а не месяцы. Из своего опыта вы делаете вывод, что вся разработка одинаковая. А она разная.

За код потока спасибо!


Однако, я имел в виду наоборот: есть два потока, А и В. автомат сначала работает на потоке А, потом работает на потоке В, потом возвращается на А. Зачем это нужно? Это нужно для вывода чего-нибудь в главное окно программы. В данном случае В — это главный поток и только в нем разрешена отрисовка. На корутинах это делается элементарно:


// Здесь мы в потоке A
withContext(UI) {
  // А здесь уже в потоке В
  window.draw(circle)
}
// И снова в А

Конвейерная модель ближе к реальным условиям, когда параллельные объекты не «умирают», выполнив работу/вычисления, чтобы потом опять «родиться»

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


Первый переход соответствует их параллельному исполнению. Второй переход идет за первым.

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


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

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


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


Но это не главная здесь проблема, т.к. «действия» оттестировать, чтобы они не выбрасывали исключений, думаю, не такая уж большая проблема (в конце концов, если существует опасность того же деления на ноль, то — проверяйте).

Возьмём пример с загрузкой файла по сети. На середине передачи связь оборвалась и операция загрузки выкинула ошибку. Как такое можно предотвратить до начала загрузки?


как доказать эквивалентность схемы одному автомату (т.е. соответствии схемы структурному параллелизму).

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


  1. В конкурентной среде, с недетерминированным временем запуска и завершения
  2. При возникновении ошибки.

Это абсолютно другая модель, которая не покрывается АП, но соответствует тому бардаку, что творится в современных ОС.

Вы снова очень остроумно уходите от ответа.


Ваши исключения — это уровень языка высокого уровня, на ассемблере, насколько я припоминаю, этого фактически нет. Не использую их и я.

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


я могу без каких-либо помех и проблем использовать любой код. Еще раз — любой! Лишь бы был только возможен доступ к нему на уровне того же С++.

Пусть теперь этот "доступный" код выкидывает исключение...


Есть многопоточность и потоки легко используются в АП (есть ситуации, когда это просто необходимо). Потоки в этом смысле очень удобны.

Код? Ибо вы переспросили меня "что значит перенос автомата на другой поток", из чего я сделал вывод, что автоматы и потоки не совместимы. Буду рад узнать, что я ошибся и перенос автомата на другой поток — допустимая операция.


Зря Вы домысливаете… ;). Я, конечно, изучил материал, хотя и «по диагонали». Просто потому, что все эти проблемы с goto давно пройденный этап.

Что подтверждает, что статью вы не читали. Она названа, на минутку, "Structured concurrency or: go statement considered harmful" в качестве омажа на известные статьи Дейкстры по структурному программированию. Продолжая его логику, автор статьи делает аналогичные выводы по поводу асинхронного программирования и необходимости структуры. Прочтите, всё-таки, статью, а после, жду кода отмены графа автоматов, то есть поддержки структурной конкурентности.


И спасибо, что напомнили


Автоматному программированию на уровне модели управления не нужны не только они, но вообще любые другие операторы управления — while, if, switch и т.д. и т.п.

вы же понимаете, что это означает, что даже структурное программирование не поддерживается АП? И мы возвращаемся во времена до Дейкстры. И нет, это не новость для меня. Я уже говорил, что АП — пройденный этап и его нужно закопать...


Уже не раз упоминался «автоматный MATLAB». У последнего, может, пользователь не меньше, чем у того же Kotlin-а.

У матлаба или у его подмножества, поддерживающего АП?


Кстати, раз уж вы завели разговор про популярность, сложите Python, C#, C++20, JavaScript, Kotlin, Go, Raku, Clojure, различные лиспы (скоро добавятся Java и Rust), чтобы оценить распространённость корутин. И это я перечислил только языки, про которые знаю, что они поддерживают корутины. Наверняка существуют еще сотни языков, которые корутины поддерживают корутины, о которых я даже не слышал.


Отвечая на ваш вопрос:


Декларируется чисто последовательное исполнение: — остановили одно — запустили другое, остановили то, вернулись назад и т.д. и т.п. Или я что-то напутал?

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


coroutineScope {
  launch {
    println("1")
    println("11")
    println("111")
  }
  launch {
    println("2")
    println("22")
    println("222")
  }
}
println("3")

то 11 будет распечана всегда после 1, 22 всегда после 2, и так далее. Но! Нет гарантий, что 2 будет всегда распечатан после 1. Последовательность запуска корутин и вообще одновременность их работы не гарантируется. Что гарантируется, так это то, что 3 будет распечатана и после 111, и после 222. Структурная конкурентность гарантирует, что основная корутина ждёт окончания работы обоих дочерних корутин.

Вы же понимаете, что вы не ответили на вопрос, а только ушли от ответа? Я вам уже кидал ссылку на постановку задачи: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/, но вы эту ссылку, как и всё, что не соответствует вашему убеждению "корутины — это автоматы, но беднее", проигнорировали. Я вам также говорил, что генерация автоматов — это только один способ реализации корутин, но вы игнорируете существование корутин, которые реализованы альтернативным способом, потому что это не покрывается вашей моделью "корутины — это автоматы, но беднее".


И нет, отсылки к моделям не подойдут. И вот почему: модель хороша ровно до тех пор, пока она описывает реальный мир с нужной точностью. Как только находится свидетельство, противоречащее модели, модель необходимо дополнить (об этом я тоже вам говорил и был проигнорирован). В реальном мире ошибки случаются. Случаются в неожиданных местах. Если модель не учитывает возможность их возникновения, то она удобна только для доказательства свойств сущностей, описываемых этой моделью (никто, кроме узкого круга теоретиков не пользуется машиной Тьюринга), но неудобна при реальном использовании. Поэтому, например, исключения получили распространение — иначе почти каждую операцию надо было бы проверять, например, при доступе к элементам массива пришлось бы проверять его границы. В языках вроде Java, проверки делаются за программиста, и в случае ошибки кидается исключение. Программист сам решает, что сделать с этим исключением — обработать в этой конкретной функции или передать выше по стеку вызовов.


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


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


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


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


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


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


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

Давайте я редуцирую ваш комментарий по моему правилу: нет кода отмены графа атвоматов -> пустой звук.

Давайте я редуцирую ваш комментарий по моему правилу: нет кода отмены графа атвоматов -> пустой звук.


PS: https://lmgtfy.com/?q=dining+philosophers+with+coroutines

  1. "автоматная модель не включает в себя обработку ошибок" (https://habr.com/ru/post/489664/#comment_21319926).
  2. "Если модель универсальна, то она моделирует все, что Вы только можете себе представить. В том числе и любые отказы и откаты."

Их этих двух утверждений следует, что автоматная модель не универсальна. QED. И как бы вы не выкручивались, без кода отмены всего графа автоматов (structured concurrency) ваши слова — пустой звук. Ваши заявления требуют доказательств, тогда как я свои предоставил.


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

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


И откуда вдруг c = res[0] и a = res[1]? И почему так, а не в другом порядке?

Давайте я переформулирую задачу, сделав её более абстрактной, и, надеюсь, более понятной для вас. Заменим вычисления a+b и b+c на F(a,b) и G(b,c). Пусть теперь количество тактов, которое занимают F и G неизвестно заранее. Препишите автомат так, чтобы он не зависел от таймингов F и G.


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


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


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

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


Ну серьёзно, что это за модель, которая не моделирует реальный мир с его отказами! И автор ещё заявляет о "жизненных реалиях"...

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


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


Думаю, уже в процессе поиска ответа (в сети Интернет, разумеется) на первый вопрос вы поймете ответ на второй. А третий уже покажет вам, что не покрывается автоматной моделью.

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

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

Я готов оспорить это утверждение для (почти) любого языка программирования. Следите за руками:


1/0

И да, замена


var res = listOf(async { a+b }, async{ b+c }).map { it.await() }
c = res[0]
a = res[1]

на


listOf(async { с = a+b }, async{ а = b+c })

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


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


Что я нахожу ироничным, так это то, что вы переизобрели продолжения (continuation), которые были в лиспе с незапамятных времен. Смотрите:


class FSumABC :
    public LFsaAppl
{
public:
    // Эти две функции можно объединить
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FSumABC(nameFsa); }
    bool FCreationOfLinksForVariables();
    // Тривиальный конструктор
    FSumABC(string strNam);
    // Сохраненные переменные
    CVar *pVarA;        //
    CVar *pVarB;        //
    CVar *pVarC;        //
    CVar *pVarStrNameA;        //
    CVar *pVarStrNameB;        //
    CVar *pVarStrNameC;        //
protected:
    // Функция вычисления
    void y1();
};

Сравните с тем, что генерирует котлиновский компилятор для suspend лямбды (из внутренней документации по кодогенерации корутин)


1. supertypes: `kotlin/coroutines/jvm/internal/SuspendLambda` and `kotlin/jvm/functions/Function{N}`
1. package-private captured variables
1. private label field of int. Private, since it is used only in the lambda itself.
1. private parameter fields. The reason of visibility is the same, as for `label` field.
1. private fields for spilled variables. Same.
1. public final method `invokeSuspend` of type `(Ljava/lang/Object;)Ljava/lang/Object;`.
It overrides `BaseContinuationImpl`'s `invokeSuspend`.
1. public final `create` of type `(<params>,Lkotlin/coroutines/Continuation)Lkotlin/coroutines/Continuation`.
`<params>` types are erased. In other words, their types are `Ljava/lang/Object;` as long as the number of parameters
is either 0 or 1. This is because the method overrides base class's `create`.
1. public final `invoke` of type `(<params>,Ljava/lang/Object;)Ljava/lang/Object;`. Since it overrides `Function{N}`'s
`invoke`, types of `<params>` are erased.
1. public or package-private constructor: `<init>` of type `(<captured-variables>,Lkotlin/coroutines/Continuation;)V`,
where we call `SuspendLambda`'s constructor with arity and completion and initlialize captured variables.
The arity is known at compile-time, but the completion is provided as argument to the constructor.

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


CVar* ... -> захваченные переменные, параметры и сохраненные локальные переменные
LFsaAppl -> SuspendLambda
y1 -> invokeSuspend
Create и FCreationOfLinksForVariables -> create
FSumABC -> <init>, конструктор

Есть ещё функция invoke, которая позволяет вызвать корутину как функцию.


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

https://pl.kotl.in/Z08HIZFVu меняйте код по своему усмотрению. Я уверен в том, что поведение не изменится, потому что изменение переменных всегда происходит в главной корутине и когда нет других работающих корутин. То есть, состояния гонки возникнуть в принципе не может.

Выйдет как раз


true, true
false, false
true, true
false, false
true, true
false, false
true, true
false, false
true, true
false, false
true, true

Я просто не понимаю, что вы хотите узнать.

Там цикл же от 0 до 1 включительно. Если нужен бесконечный цикл, можно использовать while(true) вместо for цикла.

Пожалуйста:


import kotlinx.coroutines.*

suspend fun main() =
    // Structured concurrency: if any child coroutine fails,
    // everything else will be cancelled
    coroutineScope {
        var nS = false
        var nR = false
        // Use default thread pool 
        withContext(Dispatchers.Default) {
            var Q = false
            var nQ = false
            for (i in 0..1) {
                val res = listOf(async { !(nQ&&nS) }, async { !(nR&&Q) }).map { it.await() }
                Q = res[0]
                nQ = res[1]
                println("$Q, $nQ")
                nS = true
                nR = true
            }
        }
    }

И вот результат:


true, true
false, false

listOf(async { !(nQ&&nS) }, async { !(nR&&Q) }).map { it.await() } запускает обе корутины и ждет, пока обе завершатся. Запуск корутины — дешевая операция и нет причины держать пул корутин, как в случае с потоками.

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

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

Ох, ну и полотно. Сравните с кодом на котлиновских корутинах (взят пример из статьи):


import kotlinx.coroutines.*

suspend fun main() =
    // Structured concurrency: if any child coroutine fails,
    // everything else will be cancelled
    coroutineScope {
        val a = 1
        val b = 2
        val c = 3
        val d = 4
        val e = 5
        val f = 6
        val q = 7
        val h = 8
        // Use default thread pool 
        withContext(Dispatchers.Default) {
            // t1 = a+b; t2 = c+d; t3 = e*f; t4 = q+h;
            // Ярус0: t1; t2; t3; t4;
            val t1 = async { a + b }
            val t2 = async { c + d }
            val t3 = async { e * f }
            val t4 = async { q + h }
            // Ярус1: t5 = t1*t2; t6 = t3+t4;
            // Run this ярус on different thread pool, just because we can
            val t5 = async(Dispatchers.IO) { t1.await() * t2.await() }
            val t6 = async(Dispatchers.IO) { t3.await() + t4.await() }
            // Ярус2: t7 = t5+t6;
            val t7 = async { t5.await() + t6.await() }
            println(t7.await())
        }
    }

Всё как у вас на картинке. Всё по ярусам. Основная корутина ждет только t7.await, то есть "Ярус2". "Ярус2" ждет "Ярус1", а "Ярус1" — "Ярус0".


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


Еще, возможность малой кровью прыгать между потоками дорогого стоит. Мне не надо руками задавать, какие переменные мне надо передать, мне не надо пихать их в структуру, мне не надо руками передавать структуру в другой поток, мне не надо считывать эти переменные из структуры.
Покажите, сколько кода займет перевод вашего автомата на другой поток. И вообще, допускает ли модель такое?


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

Information

Rating
Does not participate
Location
Долгопрудный, Москва и Московская обл., Россия
Date of birth
Registered
Activity