Comments 37
Потоки же (и сопрограммы в частности, как один из способов синхронизации) позволяют эффективно распараллеливать в большом, а потому явно полезнее автоматов.
Да и с точки зрения практики программирования давно известны универсальные методы преобразования обычных программ в автоматные без потери любой «эффективности». Так что о каких-либо «потерях» также говорить не приходится.
Я конечно извеняюсь, но картинки как картинки ничего особенного, а текст так вообще вода водой. Вместо тысячи слов и 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(); }
А вот картинки:
Здесь крутится порядка 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. Но и там — выбор нужной операции и определенные проверки. Например, для того же деления — проверка на ноль. Автомат универсальный, т.к. настраивается на ту или иную операцию. От этого и «распух».
Если уж мне выбирать, какой код писать и поддерживать в течении многих лет, то я не задумываясь выберу код на корутинах, ибо в нем эти самые ярусы явно видны по коду.Здесь другая технология. Я сопровождаю код
Покажите, сколько кода займет отмена всего дерева, если одна из операций выкинет исключение (например, деление на ноль). Допускает ли модель вообще какую-либо обработку ошибок?Мне незачем отменять «дерево». Его попросту нет. Обработка же ошибок — это забота отдельного компонента, т.е. автомата. В нашем случае она простейшая — не пропустить ноль в случае операции деления. Т.е. автоматная модель не включает в себя обработку ошибок. С ее точки зрения объекты от рождения правильные. Их «лечение» — это уровень реализации.
Например. Честное слово, я даже не знал, как поведет себя программа, если введу операцию деления и 0 для операнда. Оказалось — все нормально. Когда-то я, видимо, это уже учел. Если нет, то я поправил бы код автомата и далее подобная ошибка не могла бы, как сейчас, кстати, возникнуть в принципе. Короче, нужна обработка ошибок — планирую ее на уровне С++ и его средствами.
Покажите, сколько кода займет перевод вашего автомата на другой поток. И вообще, допускает ли модель такое?Такой проблемы просто нет. Но можно «прыгать» не между потоками, а между автоматными пространствами с разным дискретным временем. Это — без проблем. Без всякого кода и даже «на лету».
Как только вам надоест писать кучу кода, чтобы выполнить базовые вещи, вроде той же обработки ошибок или переноса вычислений на другой поток, вы поймете, почему корутины сегодня в моде.Обработка ошибок локализована в одном месте и потому каких-то проблем не вызывает. И может делаться по мере их возникновения (если вдруг было что-то не учтено) и затем работает всегда и в любой конфигурации, для создания которой писать код не надо. Код пишется только для новых компонент. А поскольку со временем библиотека прикладных объектов только растет, то необходимость программирования только сокращается. Вот как в данном случае. Я даже было забыл, что такой объект есть. А, как вспомнил, так сразу и «написал». Результат — на картинках.
Да. В начала коментов я дал «рекламу» вебинару по 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
Я просто не понимаю, что вы хотите узнать.
Это важно.
Поэтому
Выйдет как раз
Это Ваше предположение или Вы это получили? И в течении какого времени Вы эту картинку наблюдали? Нужно подольше.
https://pl.kotl.in/Z08HIZFVu меняйте код по своему усмотрению. Я уверен в том, что поведение не изменится, потому что изменение переменных всегда происходит в главной корутине и когда нет других работающих корутин. То есть, состояния гонки возникнуть в принципе не может.
А так все становится понятнее.
Принцип работы с переменными мне представляется таким, как реализовано в любом ПЛК — в пределах такта новые значения «внешних» переменных запоминаются, чтобы затем использоваться в следующем такте.
Здесь «тактом» корутин служит, видимо, итерация цикла?
А, кстати, корутины могут вызываться/создаваться рекурсивно?
"Тактом" во всех императивных языках программирования является оператор.
С реализацией в ПЛК этот код не имеет ничего общего кроме имён некоторых переменных и результата работы.
Вы видите согласованный вывод не потому что так реализовано в языке, а потому что так написано в программе. Можно написать так, чтобы всё поломалось, но зачем?
В смысле устранения гонок такой подход универсален. Он аналогичен приему, который используется в так называемой Основной модели для последовательностных машин (см., например, Миллер. Последовательностные схемы и машины. т.2). Там так тоже избавляются от гонок, которые порождает Комбинационная часть.
Осознанно или случайно, но в корутинах, похоже, реализовали именно подобную схему. Но это, конечно, мои догадки. Что там «по капотом» только одному «корутинному Богу» известно :) Но очень похоже, судя по пояснениям и внешнему эффекту, именно на подобную идею, реализуемую Основной моделью. И это в плюс корутинам.
Извините. а можете объяснить чуть ближе к железу и поправить, если я где неправ? Обобщённой задачей стоит то, что программист хочет писать понятный, линейный код, не заботясь о планировке параллельного или конкурентного исполнения, и при этом чтобы код выполнялся за минимальное время. Традиционным решением второго пункта является использование потоков и сопрограмм. Потоки дороги в запуске, синхронизации и объединении, поскольку стучатся в планировщик ОС, зато могут выполняться как на одном ядре последовательно, так и на нескольких параллельно. Сопрограмма не дёргает планировщик ОС, поэтому дешевле, но по классике они будут драться за ресурсы одного ядра и программист должен явно или неявно написать аналог простенького планировщика. Вы предлагаете менее стандартный путь: есть некоторый движок, в котором уже есть планировщик, самостоятельно раскидывающий потоки и ресурсы индивидуальных ядер, программисту остаётся только написать вычислительные модули (в данной парадигме — автоматы) и связать их с соседями, при этом отказавшись от формата представления "линейный текст". Так?
Нужно еще уточнить понятие «линейный текст». Я так понимаю, что это некий алгоритмический процесс во времени. Не понимаю, как от этого можно отказаться. Подобный процесс можно только представить в той или иной форме. Сейчас это обычно блок-схемная модель. У меня — автомат. В этом их качественное отличие. Автомат — более «высокая» форма представления процессов. Выше, чем блок-схемы и уж, конечно, выше, чем даже корутины (даже в том представлении, на котором мы наконец-то остановились. Я так думаю! :) ).
Он возникает, когда появляются тормоза в основном потоке
Я немного не понял, как обрабатывается ошибка потока исполнения. Можно кинуть исключение, но тогда планировщику добавится много работы. Чтобы обрабатывать "на выходе потока", в ФП мы используем монаду Maybe, в ФП, привнесенном в ООП языки — future/promise. А тут как? И как обрабатывается IO, если ввод данных ожидается на нескольких ярусах?
Нужно еще уточнить понятие «линейный текст»
В смысле листинг, как на тех же плюсах. Программирование в Simulink и LabView не особо удобное (впрочем, я не очень долго учился его готовить), всегда тянет или приходится писать код внутри блока, в итоге эти сниппеты сложно запомнить и переиспользовать. Из UML генерируется довольно похабный код.
Я немного не понял, как обрабатывается ошибка потока исполнения. Можно кинуть исключение,...Как я уже сказал, за обработку ошибок отвечает С++ в рамках отдельного автомата. Ошибка локализована в рамках его методов. Я лично этим не занимаюсь, т.к. предпочитаю исключить ошибки, а не бороться с ними, когда они уже произошли. Главное, чтобы приложение не рухнуло. Так еще можно понять причину… Ради любопытства протестировал созданную ЯПФ… Я даже убрал контроль на ноль. Не «ломается» как я не издевался. Да, может выдать несуществующее значение (nan, inf), но не более того.
Что еще надо для счастливой жизни… Но, я понимаю, это лишь в моем случае. Если Вы будете программировать автомат (см. листинг), то уже Вам и решать, что и как контролировать, обрабатывать ли исключения (если они возникают) и т.д. и т.п. Главное, что это делается в рамках довольно небольшого, как правило, объекта… Пока такой «подход» не подводил…
В смысле листинг, как на тех же плюсах.В случае сложного автомата или когда есть смысл его оформить, как библиотечный элемент, то имеет смысл программировать на С++. Простой автомат есть возможность создать и не программируя. Ну это когда нужно быстро и операции у автомата достаточно простые. У меня был проект, когда я не написал ни строчки кода — все на уже существующих
У визуального программирования есть большой плюс и не менее большой минус — ограничение в возможностях по сравнению с полноценным языком. Удобно, когда есть и то и другое.
И как обрабатывается IO, если ввод данных ожидается на нескольких ярусах?А какие здесь проблемы. Все объекты ЯПФ — полноценные автоматы и ввод данных возможен не только на первом ярусе, но и на любом другом. Но это, понятно, с учетом предложенной автоматной трактовки.
Я лично этим не занимаюсь, т.к. предпочитаю исключить ошибки
Я даже убрал контроль на ноль. Не «ломается» как я не издевался
Ну, вы умный и везучий, не все так могут :) А вообще может много чего грустного может произойти, от переполнения при сложении/умножении, потери значимых данных из-за ошибок округления до внезапного деления на ноль, что приводит аварийному завершению работы. Если на выход "потока" прилетает NaN/Inf и это проверяется — ОК, проверка на выходе (промежуточном), нормально.
У визуального программирования есть большой плюс и не менее большой минус — ограничение в возможностях по сравнению с полноценным языком. Удобно, когда есть и то и другое.
Никогда не видел удобного гибрида, буду рад, если посоветуете язык и среду.
ввод данных возможен не только на первом ярусе, но и на любом другом
И как в этом случае планировщик граф выполнения строит?
Ну, вы умный и везучий, не все так могут :)Типа: Если уж вы такие умные, то почему строем не ходите? :)
Никогда не видел удобного гибрида, буду рад, если посоветуете язык и среду.Не скажу, что и я встречал… Внешне (в силу понятных причин) мне нравится MATLAB, но вот «под капот» не заглядывал. Знаю, что обещают С++, но, если честно, подозреваю, что и там все не так просто. Поэтому создал под себя свое. Но опять же у каждого своя специфика. И советовать здесь что-то — гиблое дело.
И как в этом случае планировщик граф выполнения строит?
Если под этим понимать последовательность действий, то специально ни чего не строится. Планировщик (если его можно таковым назвать) реализует параллельную работу процессов, а уж они в процессе взаимодействия «выстраивают» нужную последовательность действий (а правильнее — алгоритм). В параллельной системе можно выделить, во-первых, уровень алгоритмов процессов. Их формирует программист. Во-вторых, общий алгоритм параллельных процессов. Здесь программист создает только связи между процессами. Дальше — они сами все «строят». Если я Вас правильно понят, то в целом как-то так все «строится» у меня.
Типа: Если уж вы такие умные, то почему строем не ходите? :)
Просто я так не рискую даже микроконтроллерах, один раз весело влетел.
Планировщик (если его можно таковым назвать) реализует параллельную работу процессов, а уж они в процессе взаимодействия «выстраивают» нужную последовательность действий (а правильнее — алгоритм)
Извините, я что-то ничего не понял. Можете сделать что-то типа туториала по своей среде и движку? Просто я пока не понимаю, чем она лучше альтернатив. Сначала я подумал, что похоже на VHDL, оказалось — нет.
Извините, я что-то ничего не понял. Можете сделать что-то типа туториала по своей среде и движку?
Кратко сложно, но попробую…
Наверное, представление можно составить, представив типовой цикл проектирования приложения в среде ВКПа.
2. Собственно программирование… Программируем автоматы на С++. Примеры кода в моих статьях. Например, автомат двуместных операций, приведенный в статье. Он реальный и входит в состав «математической библиотеки». Как правило, одновременно с автоматом проектируется и диалог управления им на уровне среды. Но можно управлять автоматами и без подобных диалогов. Через его локальные переменных (см. код), с которыми среда может работать. После создания автомата средствами среды реализуем его отладку.
Это цикл повторяется для каждого автомата. В процессе проектирования автоматы тестируются и на взаимную работу.
Так, по мере создания блоков, их отдельной и совместной отладки, формируется цельное приложение.
3. Процесс отладки и доводки в рамках среды — отдельная «песня». Здесь в полной мере реализуются возможности автоматной технологии. Пошаговая отладка, визуализация состояний, тестирование на разной скорости (управление дискретным временем), отображение в реальном времени трендов локальных, глобальных переменных среды, отключение/подключение автоматов и т.д. и т.д. Это то, что описать просто сложно — в этом надо «вариться». В чем-то это напоминает проектирование на уровне Stateflow MATLAB. Нет, правда, такой красивой и удобной графики, но, если честно, то она не особенно и нужна. В сложных случаях так совсем может только мешать, т.к. С++ дает те возможности, которые чистой графикой не реализовать.
Процесс разработки завершается, как правило, созданием окончательной конфигурации, определяющей число автоматных объектов и их взаимодействие между собой. Отмечу, что она создается фактически только средствами среды. Можно конечно и жестко запрограммировать, но так много проще и гибче с точки последующего сопровождения… Конфигурация также определяет подключаемые автоматные библиотеки, число и скорость автоматных пространств, содержащих автоматы, внешний вид приложения и взаимодействие с ним.
4. В процессе документируем. Поскольку все создается в рамках единой модели, то процесс достаточно стандартизован. Разработан даже стандарт, напоминающий чем-то UML. Хотя, наверное, ближе к нему ГОСТ Р-программ.
Важно. Описанное проектирование ни как не ограничивает текущих возможности С++ и подключения других сторонних библиотек. Но, естественно, реализация ВКПа привязана к той или иной среде проектирования. Раньше это было Visual Studio и MFC. Сейчас — Qt Creator и соответственно библиотека QT. Но ядро интерпретации автоматов фактически не зависит от библиотек. Зависят только «визуальные возможности» среды ВКПа. Хотя, например, концепция сигналов/слотов и потоков библиотеки Qt добавила свою специфику. Ну лучше, считаю, подобной спецификой не злоупотреблять.
Уточню, что я понимаю под движком/ядром. Это интерпретация табличного описания автомата (см. код). Реализация автоматных пространств, управление дискретным временем и теневой памятью. Организует доступ к внутренним состояниям, реализуемым переходам и локальным переменным автоматных процессов и глобальным переменным среды. Ядро реализует также пошаговый (отладочный) режим работы процессов.
Сама среда — это графическая оболочка для работы с движком/ядром. В принципе достаточно только ядра. Это тоже, кстати, некая библиотека. Но без оболочки будет работать много сложнее. Например Qt — библиотека без оболочки. А тот же Stateflow — это фактически оболочка для визуального автоматного программирования.
ЯПФ, конвейер, автоматные вычисления и опять… корутины