Pull to refresh

Comments 37

Это не реклама, а информация для тех, кто интересуется автоматным программирование, визуальным программированием на их базе (см. также мои ссылки на Stateflow в статье), и тем, кто в силу каких-то причин не верит в возможности автоматов, хочу привести ссылку на страничку вебинара: РАЗРАБОТКА КОНЕЧНЫХ АВТОМАТОВ И УПРАВЛЯЮЩЕЙ ЛОГИКИ И ИХ ЗАПУСК НА МИКРОКОНТРОЛЛЕРАХ И ПЛИС, который состоится завтра — 25.02.2020 в 11:00.
Почему «корутина»? Всегда была «сопрограмма». Карго-культ?
Насчет культа не буду что-то утверждать, а так лучше бы узнать у тех, кто такое название поддерживает. Видимо, в новое название пытаются вложить и какой-то новый скрытый смысл.
AFAIK, сопрограмма — это просто перевод с английского термина coroutine. Термин «coroutine» появился в 58-м году прошлого столетия. Такой перевод широко использовался в русскоязычной литературе с начала 70-х. Корутина — coroutine, написанное русскими буквами. По мне — так режет глаз и ухо. Кстати, механизм сопрограмм легко и, я бы даже сказал, «естественно» моделировался на Фортране и мною использовался в практической работе на этом языке до середины 80-х.
Подход с автоматами работает лишь в малом, а в большом бесполезен. Да и в малом эффективность переключения потоков ограничивает минимальный размер автомата, а потому есть довольно узкая ниша, где такой подход применим, но из-за узости о нише имеет смысл думать лишь когда нужно на низком уровне заниматься оптимизацией, что опять таки бывает редко.

Потоки же (и сопрограммы в частности, как один из способов синхронизации) позволяют эффективно распараллеливать в большом, а потому явно полезнее автоматов.
Нужно бы дать определение, что означают «в большом» и «в малом». Но, например, с точки зрения теории программ автоматная модель программирования, как минимум, тупо эквивалентна обычно применяемой модели программирования. Без всяких исключений, т.е. и в большом и в малом.
Да и с точки зрения практики программирования давно известны универсальные методы преобразования обычных программ в автоматные без потери любой «эффективности». Так что о каких-либо «потерях» также говорить не приходится.
Я конечно извеняюсь, но картинки как картинки ничего особенного, а текст так вообще вода водой. Вместо тысячи слов и 5 картинок можно пример на с++? Я так понял на вариант с потоками/корутинами мо
Вы, похоже, типичный кодер. Но я Вас не виню. Здесь, как раз, картинки и текст — главное. Поняв их, не должно быть проблем и реализации на любом языке. Если язык, конечно, позволит это сделать. Вот в чем может быть скрыта проблема.
Я, действительно, показываю, как без потоков и корутин реализовать любой параллельный алгоритм. И суть не в картинках, а в используемой параллельной модели. Ей посвящена, в Вашем понимании, вся «вода» (почитайте и предыдущие мои статьи, кстати, чтобы напиться окончательно). И, несмотря на кажущуюся очевидность, в таком виде автоматы не реализованы ни кем и ни где.
Ну а теперь ловите остальное — код и демонстрацию картинок (среда ВКПа) исполнения. Вы сами попросили :)
Листинг автомата операций
#ifndef FARITHM2_H
#define FARITHM2_H


#include "lfsaappl.h"

class FArithm2 :
    public LFsaAppl
{
public:
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FArithm2(pTAppCore, nameFsa, pCVarFsaLibrary); }
    bool FCreationOfLinksForVariables();
    FArithm2(TAppCore *pInfo, string strNam, CVarFsaLibrary *pCVFL);
    virtual ~FArithm2(void);
    CVar	*pVarA;				// первый операнд
    CVar	*pVarB;				// второй операнд
    CVar	*pVarC;				// результат: c = op(a,b)
    CVar	*pVarOp;			// переменная-операция
    int		nOperation;			// операция - op (0-сумма, 1 - вычитание, 2-умножение, 3-деление, 4 - накопитель
    bool	bIfMin;
    CVar	*pVarMin;				//
    double	dValueMin;
    bool	bIfMax;
    CVar	*pVarMax;				//
    double	dValueMax;
protected:
    int x1();
    void y1(); void y2();

    double	dValueA;
    double	dValueB;
    double	dValueC;
friend class CDlgArithm2;
};

#endif // FARITHM2_H
#include "stdafx.h"
#include "FArithm2.h"
#include <QtMath>

static LArc TBL_Arithm2[] = {
    LArc("st",		"st","^x1",	"y2"),			// -
    LArc("st",		"s1","x1",	"y2"),			// на всяк. случай инициировать класс
    LArc("s1",		"s1","--",	"y1"),			// выполнить операцию
    LArc()
};

FArithm2::FArithm2(TAppCore *pInfo, string strNam, CVarFsaLibrary *pCVFL):
    LFsaAppl(TBL_Arithm2, strNam, nullptr, pCVFL)
{
    pTAppCore = pInfo;
    pVarA	= nullptr;				//
    pVarB	= nullptr;				//
    pVarC	= nullptr;				//
    pVarOp	= nullptr;				//
    bIfMin	= false;
    pVarMin	= nullptr;				//
    dValueMin = 0;
    bIfMax	= false;
    pVarMax	= nullptr;				//
    dValueMax = 0;
    dValueA = 0;
    dValueB = 0;
}

FArithm2::~FArithm2(void)
{
}

bool FArithm2::FCreationOfLinksForVariables() {
    CLocVar var; var.unTypeVar = CLocVar::vtDouble; var.SetDataSrc(this,0.0); var.pLFsaAppl = this; var.pLFsaType = this; var.bIfInit = true;
    CVar *pVar = pCSetLocVar->GetAddressVar("y");
    if (!pVar) { var.strName = "y";	var.strComment = "выходное значение"; pCSetLocVar->Add(var); }
    pVar = pCSetLocVar->GetAddressVar("a");
    if (!pVar) { var.strName = "a";	var.strComment = "первый операнд"; pCSetLocVar->Add(var); }
    pVar = pCSetLocVar->GetAddressVar("b");
    if (!pVar) { var.strName = "b";	var.strComment = "второй операнд"; pCSetLocVar->Add(var); }
    pVar = pCSetLocVar->GetAddressVar("c");
    if (!pVar) { var.strName = "c";	var.strComment = "результат"; pCSetLocVar->Add(var); }
    pVar = pCSetLocVar->GetAddressVar("min");
    if (!pVar) { var.strName = "min";	var.strComment = "мин. значение выхода"; pCSetLocVar->Add(var); }
    pVar = pCSetLocVar->GetAddressVar("max");
    if (!pVar) { var.strName = "max";	var.strComment = "макс. значение выхода"; pCSetLocVar->Add(var); }
    pVar = pCSetLocVar->GetAddressVar("strOp");
    if (!pVar) { var.strName = "strOp";	var.unTypeVar = CLocVar::vtString; var.strComment = "операция(string)"; pCSetLocVar->Add(var); }
// ссылка на 1-й операнд
    if (!pVarA)
        pVarA = pTAppCore->GetAddressVar(pVarPrmProc->strParam1.c_str(), this);
// ссылка на 2-й операнд
    if (!pVarB)
        pVarB = pTAppCore->GetAddressVar(pVarPrmProc->strParam2.c_str(), this);
// ссылка на переменную результата
    if (!pVarC)
        pVarC = pTAppCore->GetAddressVar(pVarPrmProc->strParam3.c_str(), this);
// ссылка на переменную-операцию
    if (!pVarOp)
        pVarOp = pTAppCore->GetAddressVar(pVarPrmProc->strParam4.c_str(), this);
// установкa типа операции
    if (pVarOp) {
        int nType = int(pVarOp->unTypeVar);
        if (nType==CVar::vtInteger) {
            nOperation = int(pVarOp->GetDataSrc());
        }
        else if (nType==CVar::vtString) {
            string str = pVarOp->strGetDataSrc();
            if (str == "+") nOperation = 0;
            else if (str == "-") nOperation = 1;
            else if (str == "*") nOperation = 2;
            else if (str == "/") nOperation = 3;
            else if (str == "^") nOperation = 4;
            else {
                nOperation = atoi(str.c_str());
            }
        }
    }
    else
        nOperation = atoi(pVarPrmProc->strParam5.c_str());
// установить значение 1-го операнда
//		if (!pVarA) dValueA = QString(pVarPrmProc->strParam6.c_str());
// установить значение 2-го операнда
//		if (!pVarB) dValueB = QString(pVarPrmProc->strParam7.c_str());
// установить значение границы по мин. значению выхода
    bIfMin = atoi(pVarPrmProc->strParam8.c_str());
    if (bIfMin) {
        pVarMin = pTAppCore->GetAddressVar(pVarPrmProc->strParam9.c_str(), this);
        if (!pVarMin)
            dValueMin = QString(pVarPrmProc->strParam10.c_str()).toDouble();
    }
// установить значение границы по макс. значению выхода
    bIfMax = atoi(pVarPrmProc->strParam11.c_str());
    if (bIfMax) {
        pVarMax = pTAppCore->GetAddressVar(pVarPrmProc->strParam12.c_str(), this);
        if (!pVarMax)
            dValueMax = QString(pVarPrmProc->strParam13.c_str()).toDouble();
    }
    return true;
}
// ссылки на оба операнда (a, b) есть?
int FArithm2::x1() { return pVarA&&pVarB&&pVarC; }
// выполнить двуместнуюю операцию: c = a (op) b, где op =  {+,-,*,/}
void FArithm2::y1() {
    if (pVarA)
        dValueA = pVarA->GetDataSrc();
    if (pVarB)
        dValueB = pVarB->GetDataSrc();
//	if (pVarOp)
//		nOperation = pVarOp->GetDataSrc();
    if (pVarMin)
        dValueMin = pVarMin->GetDataSrc();
    if (pVarMax)
        dValueMax = pVarMax->GetDataSrc();
    if (pVarOp) {
        int nType = int(pVarOp->unTypeVar);
        if (nType==CVar::vtInteger) {
            nOperation = int(pVarOp->GetDataSrc());
        }
        else if (nType==CVar::vtString) {
            string str = pVarOp->strGetDataSrc();
            if (str == "+") nOperation = 0;
            else if (str == "-") nOperation = 1;
            else if (str == "*") nOperation = 2;
            else if (str == "/") nOperation = 3;
            else if (str == "^") nOperation = 4;
            else {
                nOperation = atoi(str.c_str());
            }
        }
    }
    switch (nOperation) {
        case 0: dValueC = dValueA+dValueB; break;
        case 1: dValueC = dValueA-dValueB; break;
        case 2: dValueC = dValueA*dValueB; break;
        case 3:
            if (bool(dValueB)) dValueC = dValueA/dValueB;
            break;
        case 4: dValueC = qPow(dValueA, dValueB); break;
        case 5: dValueC = dValueA; break;
        case 6: dValueC = dValueB; break;
        default: break;
    }
    if (bIfMin) {
        if (dValueC<dValueMin)
            dValueC = dValueMin;
    }
    if (bIfMax) {
        if (dValueC>dValueMax)
            dValueC = dValueMax;
    }
    if (pVarC) pVarC->SetDataSrc(this, dValueC);
}
// инициировать ссылки на операнды
void FArithm2::y2() { FInit(); }


А вот картинки:
С диалогами настройки процессов
image

Вариант уже без настроек
image

Здесь крутится порядка 30-процессов. Из них 7 — это собственно автоматная сеть вычисления выражения. В ее основе только один (!) автомат, код которого приведен. Он настраивается на операцию и на ссылки на операнды.
Да, дискретный такт (см. картинку с диалогами) в данном случае равен 0,5 мсек (код проекта — debug)

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


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".


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


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


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

Спасибо за за ответ и вопросы. На такое и отвечать приятно :)
Ох, ну и полотно
Здесь, наверное, 90 процентов — работа автомата с окружением — входы, выходы, настройка связей автомата и т.п. Собственно код сосредоточен в действии y1. Но и там — выбор нужной операции и определенные проверки. Например, для того же деления — проверка на ноль. Автомат универсальный, т.к. настраивается на ту или иную операцию. От этого и «распух».
Если уж мне выбирать, какой код писать и поддерживать в течении многих лет, то я не задумываясь выберу код на корутинах, ибо в нем эти самые ярусы явно видны по коду.
Здесь другая технология. Я сопровождаю код универсального автомата. Приведенный код, кстати, написан был, наверное, минимум лет 5 тому назад. Сейчас я «собрал» пример не написав вообще ни строчки кода, а используя только возможности среды по созданию объектов и установления между ними связей. Так что в данном случае нужно сопровождать, с одной стороны, конфигурацию и, с другой стороны, код автомата. По отдельности — это много проще и, главное, надежнее.
Покажите, сколько кода займет отмена всего дерева, если одна из операций выкинет исключение (например, деление на ноль). Допускает ли модель вообще какую-либо обработку ошибок?
Мне незачем отменять «дерево». Его попросту нет. Обработка же ошибок — это забота отдельного компонента, т.е. автомата. В нашем случае она простейшая — не пропустить ноль в случае операции деления. Т.е. автоматная модель не включает в себя обработку ошибок. С ее точки зрения объекты от рождения правильные. Их «лечение» — это уровень реализации.
Например. Честное слово, я даже не знал, как поведет себя программа, если введу операцию деления и 0 для операнда. Оказалось — все нормально. Когда-то я, видимо, это уже учел. Если нет, то я поправил бы код автомата и далее подобная ошибка не могла бы, как сейчас, кстати, возникнуть в принципе. Короче, нужна обработка ошибок — планирую ее на уровне С++ и его средствами.
Покажите, сколько кода займет перевод вашего автомата на другой поток. И вообще, допускает ли модель такое?
Такой проблемы просто нет. Но можно «прыгать» не между потоками, а между автоматными пространствами с разным дискретным временем. Это — без проблем. Без всякого кода и даже «на лету».
Как только вам надоест писать кучу кода, чтобы выполнить базовые вещи, вроде той же обработки ошибок или переноса вычислений на другой поток, вы поймете, почему корутины сегодня в моде.
Обработка ошибок локализована в одном месте и потому каких-то проблем не вызывает. И может делаться по мере их возникновения (если вдруг было что-то не учтено) и затем работает всегда и в любой конфигурации, для создания которой писать код не надо. Код пишется только для новых компонент. А поскольку со временем библиотека прикладных объектов только растет, то необходимость программирования только сокращается. Вот как в данном случае. Я даже было забыл, что такой объект есть. А, как вспомнил, так сразу и «написал». Результат — на картинках.

Да. В начала коментов я дал «рекламу» вебинару по Stateflow. Сегодня в нем поучаствовал. Прям — «мед на душу». Прямо так и говорили — хватит писать код. Его нужно генерироать по модели автоматически. С точки зрения технологии программирования — сказка. Если бы они только еще кое-что учли, то я бы забросил свою ВКПа. А так есть еще чем «козырять». В целом же мое «предсказание» по поводу визуального программирования, похоже, скоро сбудется ;)

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


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

Разжевываю дальше — если вы написали конфигурацию и в ней так уж случилось затесалась ошибка, то найти строчку конфигурации дебажа абстрактную реализацию исполняющую вашу конфигурации будет не так как же как дебажить «обычный» код. Сложнее/проще/другое_прилагательное вы узнаете потом.

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

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

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

По отдельности — это много проще и, главное, надежнее.
Оценочное суждение полностью расходящееся с моим опытом «типичного кодера».

Мне незачем отменять «дерево». Его попросту нет. Обработка же ошибок — это забота отдельного компонента, т.е. автомата. В нашем случае она простейшая — не пропустить ноль в случае операции деления.

Не пропустить ноль это не обработка ошибок — это механизм предотвращения ошибок — валидация. И тут несколько вопросов:
* Что если все таки ошибка (не обязательно деление на 0) случилась
* Что является результатом вычисления 1 / 0
* Как узнать какая часть конфигурации имеет ошибку

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

Такой проблемы просто нет. Но можно «прыгать» не между потоками, а между автоматными пространствами с разным дискретным временем. Это — без проблем. Без всякого кода и даже «на лету».

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

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

Само собой разумеющееся вопросы производительности/латентности поднимать даже не целесообразно, она ж (производительность) не главное.

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

«Типичный кодер» от «типичного программиста» отличается, как токарь от инженера-конструктора. На Хабре тут выложили опрос — нужно ли программисту высшее образование. 62 процента сказали — не нужно. И только 25 — за. Остальные — раздумывают :( Дико, но мы превратились в «страну лаборантов». Программист, как минимум, — инженер. Раньше без «вышки» вы могли быть только лаборантом — старшим, ведущим, нобелевским, но… только лаборантом (как Гоша из «Москва слезам не верит»). Хочешь быть инженером — будь обязан получить образование. Ну, как токарь, претендующий на должность инженера :)

Но вернемся к «нашим баранам»… Совсем кратко. Машина Тьюринга никак не потеряла своей значимости от того, что не включает обработку ошибок и/или работу с базами данных. Мысль понятна?

Я ввел вычислительную модель. Реализовал ее на С++. Без каких-либо ограничений использования последнего. На С++ есть и обработка ошибок и работа с базами. Используйте. Да хоть корутины, если подключили соответствующую библиотеку. Да, могут быть проблемы. Но это уже Ваши проблемы. От непонимания достаточно простых вещей.

Еще раз. Для «типичных кодеров». Вычислительная модель и возможности языка программирования — разные, хотя и немного связанные между собой вещи: не всякий язык подходит для реализации той или иной модели вычислений.

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

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

Изучив, поняв — кодируйте, кодируйте… работайте с базами, локализуйте ошибки, занимайтесь проблемами производительности, валидности (коли Вы такой крутой и есть время на это) и т.п. вещами, которые на разных языках, в разных системах могут заметно отличаться и, более того, могут изменяться, совершенствоваться. Но только, замечу, строго в рамках безграничных возможностей С++ и всего сонма библиотек, способных к нему подключатся.
Всё как у вас на картинке. Всё по ярусам. Основная корутина ждет только t7.await, то есть «Ярус2». «Ярус2» ждет «Ярус1», а «Ярус1» — «Ярус0»

У меня несколько картинок. Из них — сеть, которая представляет динамический объект, т.к. в реале принимает данные и вычисляет выражение.
А как Ваша программа? Мне кажется, что она отрабатывает однократно. Или я ошибаюсь? Надо ли ее как-то зациклить, чтобы реализовать подобный режим?

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

А как в рамках приведенного примера организовать в реальном времени ввод данных и вычисление выражения по тем же «ярусам», как Вы написали?

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

Ладно… Я понял ;)
Я на основе Вашего примера «тупо» набросал следующий код.
import kotlinx.coroutines.*

suspend fun main() =
    // Structured concurrency: if any child coroutine fails,
    // everything else will be cancelled
    coroutineScope {
//        val nS = 0
//        val nR = 0
        // Use default thread pool 
        withContext(Dispatchers.Default) {
            val Q = async { !(nQ&&nS) }
            val nQ = async { !(nR&&Q) }
//            println(Q, nQ))
        }
    }

Здесь nS, nR — внешние переменные по отношению к циклу установки переменных Q и nQ. Печать последних тоже желательно вынести за цикл.
Дополните код (думаю, Вам-то это несложно). Установите переменные nS, nR сначала в ноль (как в коменте), а затем, спустя какое-то время, установите их же одновременно в 1. Какие значения примут переменные Q и nQ.

Пожалуйста:


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() } запускает обе корутины и ждет, пока обе завершатся. Запуск корутины — дешевая операция и нет причины держать пул корутин, как в случае с потоками.

Начало многообещающее…
Может все-таки так:
true, true
false, false
true, true
false, false
true, true
false, false
… и так далее

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

Так давайте попробуем и посмотрим, что получится. Заранее спасибо.
Я-то не могу ;)

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


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

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

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

Это Ваше предположение или Вы это получили? И в течении какого времени Вы эту картинку наблюдали? Нужно подольше.

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

Спасибо. Буду осваивать :)
Еще раз спасибо за ссылку. Остался вопрос — кто же этого ЗВЕРЯ назвал со-программами, то бишь, корутинами ;)
А так все становится понятнее.
Принцип работы с переменными мне представляется таким, как реализовано в любом ПЛК — в пределах такта новые значения «внешних» переменных запоминаются, чтобы затем использоваться в следующем такте.
Здесь «тактом» корутин служит, видимо, итерация цикла?
А, кстати, корутины могут вызываться/создаваться рекурсивно?

"Тактом" во всех императивных языках программирования является оператор.


С реализацией в ПЛК этот код не имеет ничего общего кроме имён некоторых переменных и результата работы.


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

Тактом может быть все то, что Вы решите таковым назвать. Нужно только определиться с его периодичностью и границами. Выше, судя по всему, такт определяется моментами обновления переменных в «главной корутине». И это, действительно, позволяет нивелировать гонки переменных, но, конечно, только тех, которые корутинами контролируются. Потому-то мы и получили то, что нужно в данном случае.
В смысле устранения гонок такой подход универсален. Он аналогичен приему, который используется в так называемой Основной модели для последовательностных машин (см., например, Миллер. Последовательностные схемы и машины. т.2). Там так тоже избавляются от гонок, которые порождает Комбинационная часть.
Осознанно или случайно, но в корутинах, похоже, реализовали именно подобную схему. Но это, конечно, мои догадки. Что там «по капотом» только одному «корутинному Богу» известно :) Но очень похоже, судя по пояснениям и внешнему эффекту, именно на подобную идею, реализуемую Основной моделью. И это в плюс корутинам.
Я конечно извеняюсь, но картинки как картинки ничего особенного, а текст так вообще вода водой. Вместо тысячи слов и 5 картинок можно пример на с++? Я так понял на вариант с потоками/корутинами можно не надеяться, да и ладно ЯПФ/КА было бы уже достаточно. Вы пытаетесь «продать» идею не мейнстримной модели параллельных вычислений и почему то сравниваете ее с корутинами/потоками хотя сами сказали что это всего лишь средства. Ну так и сравнивайте модели с моделями, а средства со средствами. Вот средств поддержки ЯПФ в с++ я в вашей статье не вижу. В свою очередь корутины это средства поддержки не только параллельных вычислений, но и асинхрнонного (основанного на событиях) программирования. То что ЯПФ/КА математически эквивалентно «обычной» модели и так очевидно, иначе мы бы имели разные типы вычислительных устройств, а вот оценку накладных расходов времени исполнения и времени программиста используя ЯПФ модель я не вижу. Т.е. вы «продаете» кота в мешке.
См. ответ выше вопроса

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

И так и не так. Автоматное ядро работает в одном потоке и в нем же реализует автоматные процессы по принципу вытесняющей многозадачности. Планируются только автоматы. Планировка элементарна и это позволяет быстро их исполнять. Программист «рисует» автоматы, не заботясь кто и как их интерпретирует. Главное, он знает, что они параллельны и могут легко и просто взаимодействовать. Параллелизм автоматных процессов (а это тоже определенные требования) гарантирует опять же ядро. Есть вариант создания автомата/автоматов на потоке/потоках, но это уже крайний почти случай. Он возникает, когда появляются тормоза в основном потоке (но это должно быть достаточно большое число параллельных автоматов).
Нужно еще уточнить понятие «линейный текст». Я так понимаю, что это некий алгоритмический процесс во времени. Не понимаю, как от этого можно отказаться. Подобный процесс можно только представить в той или иной форме. Сейчас это обычно блок-схемная модель. У меня — автомат. В этом их качественное отличие. Автомат — более «высокая» форма представления процессов. Выше, чем блок-схемы и уж, конечно, выше, чем даже корутины (даже в том представлении, на котором мы наконец-то остановились. Я так думаю! :) ).
Он возникает, когда появляются тормоза в основном потоке

Я немного не понял, как обрабатывается ошибка потока исполнения. Можно кинуть исключение, но тогда планировщику добавится много работы. Чтобы обрабатывать "на выходе потока", в ФП мы используем монаду Maybe, в ФП, привнесенном в ООП языки — future/promise. А тут как? И как обрабатывается IO, если ввод данных ожидается на нескольких ярусах?


Нужно еще уточнить понятие «линейный текст»

В смысле листинг, как на тех же плюсах. Программирование в Simulink и LabView не особо удобное (впрочем, я не очень долго учился его готовить), всегда тянет или приходится писать код внутри блока, в итоге эти сниппеты сложно запомнить и переиспользовать. Из UML генерируется довольно похабный код.

Я немного не понял, как обрабатывается ошибка потока исполнения. Можно кинуть исключение,...
Как я уже сказал, за обработку ошибок отвечает С++ в рамках отдельного автомата. Ошибка локализована в рамках его методов. Я лично этим не занимаюсь, т.к. предпочитаю исключить ошибки, а не бороться с ними, когда они уже произошли. Главное, чтобы приложение не рухнуло. Так еще можно понять причину… Ради любопытства протестировал созданную ЯПФ… Я даже убрал контроль на ноль. Не «ломается» как я не издевался. Да, может выдать несуществующее значение (nan, inf), но не более того.
Что еще надо для счастливой жизни… Но, я понимаю, это лишь в моем случае. Если Вы будете программировать автомат (см. листинг), то уже Вам и решать, что и как контролировать, обрабатывать ли исключения (если они возникают) и т.д. и т.п. Главное, что это делается в рамках довольно небольшого, как правило, объекта… Пока такой «подход» не подводил…
В смысле листинг, как на тех же плюсах.
В случае сложного автомата или когда есть смысл его оформить, как библиотечный элемент, то имеет смысл программировать на С++. Простой автомат есть возможность создать и не программируя. Ну это когда нужно быстро и операции у автомата достаточно простые. У меня был проект, когда я не написал ни строчки кода — все на уже существующих универсальных автоматах (типа рассмотренного, реализующего набор операций).
У визуального программирования есть большой плюс и не менее большой минус — ограничение в возможностях по сравнению с полноценным языком. Удобно, когда есть и то и другое.
И как обрабатывается IO, если ввод данных ожидается на нескольких ярусах?
А какие здесь проблемы. Все объекты ЯПФ — полноценные автоматы и ввод данных возможен не только на первом ярусе, но и на любом другом. Но это, понятно, с учетом предложенной автоматной трактовки.
Я лично этим не занимаюсь, т.к. предпочитаю исключить ошибки
Я даже убрал контроль на ноль. Не «ломается» как я не издевался

Ну, вы умный и везучий, не все так могут :) А вообще может много чего грустного может произойти, от переполнения при сложении/умножении, потери значимых данных из-за ошибок округления до внезапного деления на ноль, что приводит аварийному завершению работы. Если на выход "потока" прилетает NaN/Inf и это проверяется — ОК, проверка на выходе (промежуточном), нормально.


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

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


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

И как в этом случае планировщик граф выполнения строит?

Ну, вы умный и везучий, не все так могут :)
Типа: Если уж вы такие умные, то почему строем не ходите? :)
Никогда не видел удобного гибрида, буду рад, если посоветуете язык и среду.
Не скажу, что и я встречал… Внешне (в силу понятных причин) мне нравится MATLAB, но вот «под капот» не заглядывал. Знаю, что обещают С++, но, если честно, подозреваю, что и там все не так просто. Поэтому создал под себя свое. Но опять же у каждого своя специфика. И советовать здесь что-то — гиблое дело.
И как в этом случае планировщик граф выполнения строит?

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

Просто я так не рискую даже микроконтроллерах, один раз весело влетел.


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

Извините, я что-то ничего не понял. Можете сделать что-то типа туториала по своей среде и движку? Просто я пока не понимаю, чем она лучше альтернатив. Сначала я подумал, что похоже на VHDL, оказалось — нет.

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

Кратко сложно, но попробую…
Наверное, представление можно составить, представив типовой цикл проектирования приложения в среде ВКПа.

Цикл проектирования в ВКПа
1. В общих чертах составляет модель — какие автоматы нужны и их протоколы взаимодействие/протоколы. Автоматы (на структурном уровне — это некие блоки) проектируются на С++, но какие-то уже есть готовые. Последние входят в состав некоторого числа «автоматных библиотек». На случай, если автомат несложный, то его можно достаточно быстро «набросать» средствами визуального проектирования среды ВКПа (т.е. без создания сода на С++). В процессе такого структурного проектирования определяются входные/выходные каналы (локальные переменные автоматов). Созданный в соответствии с этими требованиями автомат помещается в существующую или во вновь созданную автоматную библиотеку. Автомат можно и не включать в библиотеку. Он будет немного проще, но доступа к нему на уровне среды не будет. Так в общих чертах формируется общее представление о приложении.

2. Собственно программирование… Программируем автоматы на С++. Примеры кода в моих статьях. Например, автомат двуместных операций, приведенный в статье. Он реальный и входит в состав «математической библиотеки». Как правило, одновременно с автоматом проектируется и диалог управления им на уровне среды. Но можно управлять автоматами и без подобных диалогов. Через его локальные переменных (см. код), с которыми среда может работать. После создания автомата средствами среды реализуем его отладку.
Это цикл повторяется для каждого автомата. В процессе проектирования автоматы тестируются и на взаимную работу.
Так, по мере создания блоков, их отдельной и совместной отладки, формируется цельное приложение.

3. Процесс отладки и доводки в рамках среды — отдельная «песня». Здесь в полной мере реализуются возможности автоматной технологии. Пошаговая отладка, визуализация состояний, тестирование на разной скорости (управление дискретным временем), отображение в реальном времени трендов локальных, глобальных переменных среды, отключение/подключение автоматов и т.д. и т.д. Это то, что описать просто сложно — в этом надо «вариться». В чем-то это напоминает проектирование на уровне Stateflow MATLAB. Нет, правда, такой красивой и удобной графики, но, если честно, то она не особенно и нужна. В сложных случаях так совсем может только мешать, т.к. С++ дает те возможности, которые чистой графикой не реализовать.
Процесс разработки завершается, как правило, созданием окончательной конфигурации, определяющей число автоматных объектов и их взаимодействие между собой. Отмечу, что она создается фактически только средствами среды. Можно конечно и жестко запрограммировать, но так много проще и гибче с точки последующего сопровождения… Конфигурация также определяет подключаемые автоматные библиотеки, число и скорость автоматных пространств, содержащих автоматы, внешний вид приложения и взаимодействие с ним.

4. В процессе документируем. Поскольку все создается в рамках единой модели, то процесс достаточно стандартизован. Разработан даже стандарт, напоминающий чем-то UML. Хотя, наверное, ближе к нему ГОСТ Р-программ.

Важно. Описанное проектирование ни как не ограничивает текущих возможности С++ и подключения других сторонних библиотек. Но, естественно, реализация ВКПа привязана к той или иной среде проектирования. Раньше это было Visual Studio и MFC. Сейчас — Qt Creator и соответственно библиотека QT. Но ядро интерпретации автоматов фактически не зависит от библиотек. Зависят только «визуальные возможности» среды ВКПа. Хотя, например, концепция сигналов/слотов и потоков библиотеки Qt добавила свою специфику. Ну лучше, считаю, подобной спецификой не злоупотреблять.

Уточню, что я понимаю под движком/ядром. Это интерпретация табличного описания автомата (см. код). Реализация автоматных пространств, управление дискретным временем и теневой памятью. Организует доступ к внутренним состояниям, реализуемым переходам и локальным переменным автоматных процессов и глобальным переменным среды. Ядро реализует также пошаговый (отладочный) режим работы процессов.
Сама среда — это графическая оболочка для работы с движком/ядром. В принципе достаточно только ядра. Это тоже, кстати, некая библиотека. Но без оболочки будет работать много сложнее. Например Qt — библиотека без оболочки. А тот же Stateflow — это фактически оболочка для визуального автоматного программирования.

Sign up to leave a comment.

Articles