1. Введение
Конкурентная борьба за умы, настроения и чаяния программистов является, как мне представляется, современным трендом развития программирования. Когда почти не предлагается ничего нового, хотя и под лозунгом борьбы за него. Распознать в толчее программных парадигм что-то новое, которое на поверку часто оказывается достаточно известным а, порой и просто устаревшим, весьма и весьма сложно. Все «замыливается» терминологическими изысками, многословным анализом и многострочными примерами на множестве языков программирования. При этом упорно обходятся просьбы открыть и/или рассмотреть подоплеку решения, суть нововведений, пресекаются в зародыше попытки выяснить насколько это нужно и что даст в итоге, что качественно отличает нововведение от уже известных подходов и средств программирования.
На Хабре я появился, как метко было подмечено в одной из дискуссий, после некой заморозки. Даже и не буду возражать. Как минимум, впечатление, видимо, именно такое. Поэтому согласен, каюсь, хотя, если и виноват, то лишь частично. Признаюсь, живу представлениями о параллельном программировании, сформированном в 80-х годов прошлого века. Древность? Возможно. Но скажите, что появилось нового, о чем бы не было известно уже тогда науке [параллельного] программирования (см. подробнее [1]). На тот момент параллельные программы разделили на два класса — параллельно-последовательные и асинхронные. Если первые уже считались архаичными, то вторые — продвинутым и по-настоящему параллельными. Среди последних выделяли программирование с событийным управлением (или просто событийное программирование), потоковым управлением и динамическим. Вот в общих чертах и все. Далее уже детали.
А что предлагает нынешнее программирование в дополнение к уже известному как минимум 40 лет назад? На мой «отмороженный взгляд» — ничего. Сопрограммы, как выяснилось, теперь называются корутинами или даже горутинами, термины параллелизм и конкурентность вводят в ступор, похоже, не только переводчиков. И подобным примерам несть числа. Например, в чем отличие реактивного программирования (РП) от событийного или потокового? В какую из известных категорий и/или классификаций оно попадает? Похоже, это никого не интересует и никто не может это прояснить. Или классифицировать можно теперь по названию? Тогда, действительно, сопрограммы и корутины — разные вещи, а параллельное программирование просто обязано отличаться от конкурентного. А машины состояний? Что за чудо-техника такая?
«Спагетти» в голове возникает от забвения теории, где, когда вводится новая модель, то она сравнивается с уже известными и хорошо изученными моделями. Плохо ли хорошо это будет сделано, но, по крайней мере, с этим можно разобраться, поскольку процесс формализован. Но как докопаться до сути, если сопрограммам дать новую кличку и ковырять затем «подкапотный код» одновременно на пяти языках, оценивая вдобавок перспективу миграции в потоки. И это только сопрограммы, о которых, если честно, должно уже забыть в силу их элементарности и малой пользы (речь, безусловно, о моем опыте).
2. Реактивное программирование и все, все, все
Не будем себе ставить цель разобраться досконально с понятием «реактивное программирование», хотя и возьмем за основу дальнейшего обсуждения «реактивный пример». Будет создана его формальная модель на базе известной формальной модели. И это, надеюсь, позволит наглядно, точно, в деталях разобраться с трактовкой и работой исходной программы. А вот насколько созданная модель и ее реализация будут «реактивны» — решать апологетам этого вида программирования. На текущий момент пока будет уже достаточно того, что новая модель должна будет реализовать/смоделировать все нюансы исходного примера. Если что-то будет не учтено, то, надеюсь, найдутся те, кто поправит меня.
Итак, в [2] рассмотрен пример реактивной программы, код которой приведен на листинге 1.
Листинг 1. Код реактивной программы
1. Х1 = 2
2. Х2 = 3
3. Х3 = Х1 + Х2
4. Напечатать Х1, Х2, Х3
5. Х1 = 4
6. Напечатать Х1, Х2, Х3
В мире реактивного программирования результат ее работы будет отличным от результата обычной программы такого же вида. Уже одно это плохо, если не сказать — безобразие, т.к. результат программы должен быть однозначным и не зависеть от реализации. Но больше смущает другое. Во-первых, по внешнему виду отличить обычный подобный код от реактивного вряд ли вообще возможно. Во-вторых, видимо, сам автор не совсем уверен в работе реактивной программы, говоря о результате «скорее всего». И, в-третьих, а какой из результатов считать правильным?
Подобная неоднозначность в трактовке кода привела к тому, что не сразу можно в него и «врубиться». Но затем, как часто бывает, все оказалось много проще, чем можно было бы предположить. На рис.1 приведены две структурные схемы, которые, хочется надеяться, соответствуют структуре и объясняют работу примера. На верхней схеме блоки X1 и X2 организуют ввод данных, сигнализируя блоку X3 об их изменении. Последний выполняет суммирование и разрешает блоку Pr вывести на печать текущие значения переменных. Напечатав, блок Pr сигнализирует блоку X3, причем, ему и только ему, что он готов к печати новых значений.
Рис. 1. Две структурные модели примера
Вторая схема, в сравнении с первой, совсем элементарна. В рамках единственного блока она вводит данные и реализует последовательно: 1) вычисление суммы входных данных и 2) вывод их на печать. Внутренняя начинка блока на данном уровне представления не раскрывается. Хотя можно сказать, что на структурном уровне он может быть «черным ящиком в том числе и для схемы из четырех блоков. Но все же его [алгоритмическое] устройство, как предполагается, будет другим.
Замечание. Подход к программе, как к черному ящику, по сути отражает отношение к ней со стороны пользователя. Последнего интересует не ее реализация, а результат работы. Реактивная ли это программа, событийная ли или какая-либо другая, но результат в соответствии с теорией алгоритмов должен быть однозначным и предсказуемым.
На рис. 2 представлены алгоритмические модели, которые в деталях проясняют внутреннее [алгоритмическое] устройство блоков схем. Верхняя модель представлена сетью автоматов, где каждый из автоматов — алгоритмическая модель отдельного блока. Связи между автоматами, изображенные штрихпунктирными дугами, соответствуют связям схемы. Модель из одного автомата описывает алгоритм работы структурной схемы, состоящей из одного блока (см. отдельный блок Pr на рис. 1).
Рис. 2. Алгоритмические модели для структурных схем
Автоматы X1 и X2 (имена автоматов и блоков совпадают с именами их переменных), выявляют изменения и, если автомат X3 готов к выполнению операции сложения (находится в состоянии „s0“), переходят в состояние „s1“, запоминая текущее значение переменной. Автомат X3, получив разрешение на переход в состояние „s1“, выполняет операцию сложения и, если необходимо, ждет завершения печати переменных. „Автомат печати“ Pr, завершив печать, возвращается в начальное состояние „p0“, где и ждет очередной команды. Заметим, что его состояние „p1“ запускает цепочку обратных переходов — автомата X3 в состояние „s0“, а X1 и X2 в состояние „s0“. После этого анализ входных данных, затем их суммирование и последующий вывод на печать повторяется.
В сравнению с автоматной сетью алгоритм отдельного автомата Pr совсем прост, но, отметим, делает ту же работу и, может быть, даже быстрее. Его предикаты выявляют изменение переменных. Если это произошло, то выполняется переход в состояние „p1“ с запуском действия y1 (см. рис. 2), которое суммирует текущие значения переменных, одновременно их запоминая. Далее на безусловном переходе из состояния „p1“ в состояние „p0“ действие y2 выполняет печать переменных. После этого процесс возвращается к анализу входных данных. Код реализации последней модели демонстрирует листинг 2.
Листинг 2. Реализация автомата Pr
#include "lfsaappl.h"
#include "fsynch.h"
extern LArc TBL_PlusX3[];
class FPlusX3 : public LFsaAppl
{
public:
LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FPlusX3(nameFsa, pCVarFsaLibrary); }
bool FCreationOfLinksForVariables();
FPlusX3(string strNam, CVarFsaLibrary *pCVFL): LFsaAppl(TBL_PlusX3, strNam, nullptr, pCVFL) { }
CVar *pVarY; //
CVar *pVarX1; //
CVar *pVarX2; //
CVar *pVarX3; //
CVar *pVarStrNameX1; // имя переменной X1
CVar *pVarStrNameX2; // имя переменной X2
CVar *pVarStrNameX3; // имя переменной X3
protected:
int x1(); int x2();
int x12() { return pVarX1 != nullptr && pVarX2 && pVarX3; };
void y1();
void y12() { FInit(); };
double dSaveX1{0};
double dSaveX2{0};
};
#include "stdafx.h"
#include "fplusx3.h"
LArc TBL_PlusX3[] = {
LArc("st", "st","^x12","y12"), //
LArc("st", "p0","x12", "--"), //
LArc("p0", "p1","x1", "y1"), //
LArc("p0", "p1","x2", "y1"), //
LArc("p1", "p0","--", "--"), //
LArc()
};
// creating local variables and initialization of pointers
bool FPlusX3::FCreationOfLinksForVariables() {
// creating local variables
pVarY = CreateLocVar("strY", CLocVar::vtString, "print of output string"); // печать переменной
pVarX1 = CreateLocVar("dX1", CLocVar::vtDouble, ""); // печать переменной
pVarX2 = CreateLocVar("dX2", CLocVar::vtDouble, ""); // печать переменной
pVarX3 = CreateLocVar("dX3", CLocVar::vtDouble, ""); // печать переменной
pVarStrNameX1 = CreateLocVar("strNameX1", CLocVar::vtString, ""); // имя входной переменной
pVarStrNameX2 = CreateLocVar("strNameX2", CLocVar::vtString, ""); // имя входной переменной
pVarStrNameX3 = CreateLocVar("strNameX3", CLocVar::vtString, ""); // имя входной переменной
// initialization of pointers
string str;
str = pVarStrNameX1->strGetDataSrc();
if (str != "") { pVarX1 = pTAppCore->GetAddressVar(str.c_str(), this); }
str = pVarStrNameX2->strGetDataSrc();
if (str != "") { pVarX2 = pTAppCore->GetAddressVar(str.c_str(), this); }
str = pVarStrNameX3->strGetDataSrc();
if (str != "") { pVarX3 = pTAppCore->GetAddressVar(str.c_str(), this); }
return true;
}
int FPlusX3::x1() { return pVarX1->GetDataSrc() != dSaveX1; }
int FPlusX3::x2() { return pVarX2->GetDataSrc() != dSaveX2; }
void FPlusX3::y1() {
// X3 = X1 + X2
double dX1 = pVarX1->GetDataSrc(); double dX2 = pVarX2->GetDataSrc();
double dX3 = dX1 + dX2;
pVarX3->SetDataSrc(this, dX3);
dSaveX1 = dX1; dSaveX2 = dX2;
// Напечатать Х1, Х2, Х3
QString strX1; strX1.setNum(dX1); QString strX2; strX2.setNum(dX2);
QString strX3; strX3.setNum(dX3);
QString qstr = "X1=" + strX1 + ", X2=" + strX2 + ", X3=" + strX3;
pVarY->SetDataSrc(nullptr, qstr.toStdString(), nullptr);
}
Объем кода явно несравнимо больше исходного примера. Но, заметим, не кодом единым. Новое решение снимает все вопросы по функционированию, не давая разыграться фантазиям в трактовке работы программы. Пример, который выглядит компактно и элегантно, но о работе которого можно сказать „скорее всего“, не вызывает, скажем так, положительных эмоций и желания с ним работать. Следует также учесть, что сравнивать надо фактически с действием автомата у1.
Остальной код связан с требованиями „автоматного окружения“, о котором, замечу, в исходном коде не говорится ни слова. Так, метод FCreationOfLinksForVariables базового автоматного класса LFsaAppl создает локальные переменные для автомата и ссылки на них, когда на уровне среды ВКПа будут указаны символьные имена связанных с ними других переменных среды. Первый раз он запускается при создании автомата, а затем в рамках метода FInit (см. действие y12), т.к. не все ссылки известны при создании объекта. Автомат будет находиться в состоянии „st“ до тех пор, пока не будут инициализированы все необходимые ссылки, которые проверяет предикат x12. Ссылку на переменную, если задано ее имя, возвращает метод GetAddressVar.
Чтобы снять возможные вопросы, приведем и код автоматной сети. Он представлен на листинге 3 и включает код трех автоматных классов. Именно на их основе создается множество объектов, которые соответствуют структурной схеме сети, показанной на рис. 1. Обратим внимание, что объекты X1 и X2 порождены от общего класса FSynch.
Листинг 3. Классы автоматной сети
#include "lfsaappl.h"
extern LArc TBL_Synch[];
class FSynch : public LFsaAppl
{
public:
double dGetData() { return pVarX->GetDataSrc(); };
LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FSynch(nameFsa, pCVarFsaLibrary); }
bool FCreationOfLinksForVariables();
FSynch(string strNam, CVarFsaLibrary *pCVFL): LFsaAppl(TBL_Synch, strNam, nullptr, pCVFL) { }
CVar *pVarX; // вход
CVar *pVarStrNameX; // имя входной переменной
CVar *pVarStrNameObject;// имя объекта-функции
LFsaAppl *pL {nullptr};
protected:
int x1() { return pVarX->GetDataSrc() != dSaveX; }
int x2() { return pL->FGetState() == "s1"; }
int x12() { return pL != nullptr; };
void y1() { dSaveX = pVarX->GetDataSrc(); }
void y12() { FInit(); };
double dSaveX{0};
};
#include "stdafx.h"
#include "fsynch.h"
LArc TBL_Synch[] = {
LArc("st", "st","^x12","y12"), //
LArc("st", "s0","x12", "y1"), //
LArc("s0", "s1","x1", "y1"), //
LArc("s1", "s0","x2", "--"), //
LArc()
};
// creating local variables and initialization of pointers
bool FSynch::FCreationOfLinksForVariables() {
// creating local variables
pVarX = CreateLocVar("x", CLocVar::vtDouble, "локальный вход");
pVarStrNameX = CreateLocVar("strNameX1", CLocVar::vtString, "name of external input variable(x1)"); // имя входной переменной
pVarStrNameObject = CreateLocVar("strNameObject", CLocVar::vtString, "name of function"); // имя функции
// initialization of pointers
string str;
if (pVarStrNameX) {
str = pVarStrNameX->strGetDataSrc();
if (str != "") { pVarX = pTAppCore->GetAddressVar(str.c_str(), this); }
}
str = pVarStrNameObject->strGetDataSrc();
if (str != "") { pL = FGetPtrFsaAppl(str); }
return true;
}
#include "lfsaappl.h"
#include "fsynch.h"
extern LArc TBL_X1X2X3[];
class FX1X2X3 : public LFsaAppl
{
public:
double dGetData() { return pVarX3->GetDataSrc(); };
LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FX1X2X3(nameFsa, pCVarFsaLibrary); }
bool FCreationOfLinksForVariables();
FX1X2X3(string strNam, CVarFsaLibrary *pCVFL): LFsaAppl(TBL_X1X2X3, strNam, nullptr, pCVFL) { }
CVar *pVarX1{nullptr}; //
CVar *pVarX2{nullptr}; //
CVar *pVarX3{nullptr}; //
CVar *pVarStrNameFX1; // объект X1
CVar *pVarStrNameFX2; // объект X2
CVar *pVarStrNameFPr; // объект Pr
CVar *pVarStrNameX3; // имя выходной переменной
FSynch *pLX1 {nullptr};
FSynch *pLX2 {nullptr};
LFsaAppl *pLPr {nullptr};
protected:
int x1() { return pLX1->FGetState() == "s1"; }
int x2() { return pLX2->FGetState() == "s1"; }
int x3() { return pLPr->FGetState() == "p1"; }
int x12() { return pLPr != nullptr && pLX1 && pLX2 && pVarX3; };
void y1() { pVarX3->SetDataSrc(this, pLX1->dGetData() + pLX2->dGetData()); }
void y12() { FInit(); };
};
#include "stdafx.h"
#include "fx1x2x3.h"
LArc TBL_X1X2X3[] = {
LArc("st", "st","^x12","y12"), //
LArc("st", "s0","x12", "--"), //
LArc("s0", "s1","x1", "y1"), //
LArc("s0", "s1","x2", "y1"), //
LArc("s1", "s0","x3", "--"), //
LArc()
};
// creating local variables and initialization of pointers
bool FX1X2X3::FCreationOfLinksForVariables() {
// creating local variables
pVarX3 = CreateLocVar("x", CLocVar::vtDouble, "локальный выход");
pVarStrNameFX1 = CreateLocVar("strNameFX1", CLocVar::vtString, "");
pVarStrNameFX2 = CreateLocVar("strNameFX2", CLocVar::vtString, "");
pVarStrNameFPr = CreateLocVar("strNameFPr", CLocVar::vtString, "");
pVarStrNameX3 = CreateLocVar("strNameX3", CLocVar::vtString, "");
// initialization of pointers
string str; str = pVarStrNameFX1->strGetDataSrc();
if (str != "") { pLX1 = (FSynch*)FGetPtrFsaAppl(str); }
str = pVarStrNameFX2->strGetDataSrc();
if (str != "") { pLX2 = (FSynch*)FGetPtrFsaAppl(str); }
str = pVarStrNameFPr->strGetDataSrc();
if (str != "") { pLPr = FGetPtrFsaAppl(str); }
return true;
}
#include "lfsaappl.h"
#include "fsynch.h"
extern LArc TBL_Print[];
class FX1X2X3;
class FPrint : public LFsaAppl
{
public:
LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FPrint(nameFsa, pCVarFsaLibrary); }
bool FCreationOfLinksForVariables();
FPrint(string strNam, CVarFsaLibrary *pCVFL): LFsaAppl(TBL_Print, strNam, nullptr, pCVFL) { }
CVar *pVarY; // выход
CVar *pVarStrNameFX1; // имя объекта типа X1
CVar *pVarStrNameFX2; // имя объекта типа X2
CVar *pVarStrNameFX3; // имя объекта типа X3
FSynch *pLX1 {nullptr}; // указатель на объект X1
FSynch *pLX2 {nullptr}; // указатель на объект X2
FX1X2X3 *pLX3 {nullptr}; // указатель на объект X3
protected:
int x1();
int x12() { return pLX3 != nullptr && pLX1 && pLX2 && pLX3; };
void y1();
void y12() { FInit(); };
};
#include "stdafx.h"
#include "fprint.h"
#include "fx1x2x3.h"
LArc TBL_Print[] = {
LArc("st", "st","^x12","y12"), //
LArc("st", "p0","x12", "--"), //
LArc("p0", "p1","x1", "y1"), //
LArc("p1", "p0","--", "--"), //
LArc()
};
// creating local variables and initialization of pointers
bool FPrint::FCreationOfLinksForVariables() {
// creating local variables
pVarY = CreateLocVar("strY", CLocVar::vtString, "print of output string"); // печать переменной
pVarStrNameFX1 = CreateLocVar("strNameFX1", CLocVar::vtString, "name of external input object(x1)"); // имя входной переменной
pVarStrNameFX2 = CreateLocVar("strNameFX2", CLocVar::vtString, "name of external input object(x2)"); // имя входной переменной
pVarStrNameFX3 = CreateLocVar("strNameFX3", CLocVar::vtString, "name of external input object(pr)"); // имя входной переменной
// initialization of pointers
string str;
str = pVarStrNameFX1->strGetDataSrc();
if (str != "") { pLX1 = (FSynch*)FGetPtrFsaAppl(str); }
str = pVarStrNameFX2->strGetDataSrc();
if (str != "") { pLX2 = (FSynch*)FGetPtrFsaAppl(str); }
str = pVarStrNameFX3->strGetDataSrc();
if (str != "") { pLX3 = (FX1X2X3*)FGetPtrFsaAppl(str); }
return true;
}
int FPrint::x1() { return pLX3->FGetState() == "s1"; }
void FPrint::y1() {
QString strX1; strX1.setNum(pLX1->dGetData());
QString strX2; strX2.setNum(pLX2->dGetData());
QString strX3; strX3.setNum(pLX3->dGetData());
QString qstr = "X1=" + strX1 + ", X2=" + strX2 + ", X3=" + strX3;
pVarY->SetDataSrc(nullptr, qstr.toStdString(), nullptr);
}
Этот код от кода листинга 1 отличается, как картинка самолета от его конструкторской документации. Но, думаю, мы прежде всего Программисты, а, не в обиду будет им сказано, какие-нибудь дизайнеры. Наш „конструкторский код“, должен быть легким для понимания и однозначно трактуемым, чтобы наш „самолет“ не рухнул при первом же полете. А уж если такая беда случилась, а с программами это происходит явно чаще, чем с самолетами, то чтобы причину можно было найти легко и быстро.
Поэтому, рассматривая листинг 3, необходимо представлять, что число классов не связано напрямую с количеством соответствующих объектов в параллельной программе. Код не отражает и связи между объектами, но содержит механизмы, которые их создают. Так, класс FSynch содержит указатель pL на объект типа LFsaAppl. Имя данного объекта определяется локальной переменной, которой в среде ВКПа будет соответствовать переменная автомата с именем strNameObject. Указатель необходим, чтобы с помощью метода FGetState контролировать текущее состояние автоматного объекта типа FSynch (см. код предиката x2). Аналогичные указатели на объекты, переменные для указания имен объектов, и предикаты, необходимые для организации связей, содержат и остальные классы.
Теперь несколько слов по поводу „конструировании“ параллельной программы в среде ВКПа. Она создается в процессе загрузки конфигурации программы. При этом сначала создаются объекты на базе классов из тематических динамических библиотек автоматного типа (их набор определяется конфигурацией приложения/программы). Созданные объекты идентифицируются своими именам (назовем их автоматными переменными). Затем локальным переменным автоматов прописываются необходимые значения. В нашем случае переменным, имеющим строковый тип, задаются имена переменных других объектов и/или имена объектов. Так устанавливаются связи между объектами параллельной автоматной программы (см. рис. 1). Далее, изменяя значения входных переменных (используя для этого индивидуальные диалоги управления объектами или стандартный диалог/диалоги среды для задания значений переменным среды), фиксируем результат. Его можно увидеть, используя стандартный диалог среды для отображения значений переменных.
3. К анализу параллельных программ
О функционировании параллельной программы, если только она не совсем простая последовательно-параллельная, что-то конкретное сказать, как правило, весьма и весьма сложно. Не является исключением и рассматриваемая сеть автоматов. Далее мы в этом убедимся, разбираясь, что же от нее можно ждать.
Результирующий автомат и сеть, для которой он построен, показаны на рис. 3. От сети на рис. 2, кроме переименования ее элементов — автоматов, входных и выходных сигналов, ее отличает отсутствие „автомата печати“ переменных. Последний не является существенным для работы сети, а переименование позволяет применить операцию композиции для построения результирующего автомата. Кроме того, для создания более коротких имен введено кодирование, когда, например, состояние „a0“ автомата A представляется символом „0“, а „a1“ — символом „1“. Аналогично и для других автоматов. В этом случае компонентному состоянию сети, например, „a1b0c1“ присваивается имя „101“. Аналогично формируются имена для всех компонентных состояний сети, число которых определяется произведением состояний компонентных автоматов.
Рис. 3. Результирующий автомат сети
Результирующий автомат можно, конечно, вычислить чисто формальным путем, но для этого нужен соответствующий „калькулятор“. Но если его нет, то можно использовать достаточно простой интуитивный алгоритм. В его рамках фиксируется то или иное компонентное состояние сети и далее, перебирая все возможные входные ситуации, определяются „ручками“ целевые компонентные состояния. Так, зафиксировав состояние „000“, соответствующее текущим состояниям компонентных автоматов — »a0", «b0», «c0», определяются переходы для конъюнкций входных переменных ^x1^x2, ^x1x2, x1^x2, x1x2. Получим переходы соответственно в состояния «a0b0c0», «a0b1c0», «a1b0c0», «a1b1c0», которые на результирующем автомате обозначены «000», «010», «100» и «110». Подобную операцию необходимо повторить последовательно для всех достижимых состояний. При этом петли, которые не нагружены действиями, можно из графа исключить.
Что имеем «в сухом остатке». Мы достигли главного — получили результирующий автомат, который точно описывает работу сети. Выяснили, что из восьми возможных состояний сети, одно является недоступным (изолированным) — состояние «001». Это говорит о том, что операция суммирования ни при каких обстоятельствах не будет запущена для входных переменных, которые не изменили текущего значения.
Что тревожит, хотя тестирование и не выявило ошибок. На графе результирующего автомата обнаружились противоречивые по выходным действиям переходы. Они помечены комбинацией действий y1y3 и y2y3. Действия y1 и y2 запускаются, когда изменяются входные данные, и тут же другое действие y3 параллельно им вычисляет сумму переменных. Какими значениями оно будет оперировать — старыми или только что измененными новыми? Для устранения неоднозначности можно просто изменить действия y3 и y4. В этом случае их код будет следующим: X3 = X1Sav + X2Sav и print(X1Sav, X2Sav, X3).
Итак. Построение результирующего автомата выявило явные проблемы в созданной параллельной модели. Проявятся ли они в реактивной программе — вопрос. Все будет, видимо, зависеть от подхода к реализации параллелизма в реактивной парадигме. В любом случае подобную зависимость нужно обязательно учитывать и как-то исключить. В случае автоматной сети проще оставить измененный вариант, чем пытаться изменить сеть. Ничего страшного, если будут сначала распечатаны «старые» данные, инициировавшие работу сети, а уж затем следом будут распечатаны и текущие.
4. Выводы
У каждого из рассмотренных решений есть свои плюсы и минусы. Исходное совсем простое, сеть сложнее, а созданное на базе одного автомата, приступит к анализу входных данных только после их визуализации. Та же автоматная сеть в силу своего параллелизма анализ входных данных запустит еще до окончания процедуры печати. И если время визуализации большое, а на фоне операции суммирования это так и будет, то сеть будет уже быстрее с точки зрения контроля входных данных. Т.е. оценка, основанная на оценке объема кода, в случае параллельных программ не всегда объективна. Рассуждая проще, сеть — параллельна, однокомпонентное решение — во многом последовательно (его предикаты и действия параллельны). А мы, прежде всего, — про параллельные программы.
Сетевая модель — это также пример гибкого решения. Во-первых, компоненты могут проектироваться независимо друг от друга. Во-вторых, любой компонент может быть заменен на другой. И, в-третьих, любой компонент сети может быть элементом библиотеки автоматных процессов и использован в другом сетевом решении. И это только наиболее очевидные преимущества параллельного решения.
Но вернемся к реактивному программированию. Считает ли РП все операторы программы изначально параллельными? Можно лишь предположить, что без этого сложно говорить о парадигме программирования, «ориентированной на потоки данных и распространение изменений» (см. определение реактивного программирования в [3]). Но тогда в чем его отличие от программирования с потоковым управлением (о нем см. подробнее [1])? Так мы возвращаемся к тому, с чего начали: как классифицировать реактивное программирование в рамках известных классификаций? И, если РП в чем-то особенное программирование, то в чем оно отлично от известных парадигм программирования?
Ну, и о теории. Без нее анализ параллельных алгоритмов был бы не просто затруднен — невозможен. В процесса анализа иногда выявляются проблемы, о которых даже при внимательном и вдумчивом взгляде на программу, как, между прочим, на «конструкторский документ», невозможно догадаться. В любом случае я за то, чтобы самолеты, как в переносном, так и любом другом смысле, не падали. Это я к тому, что, безусловно, нужно стремиться к простоте и изяществу форм, но без потери качества. Мы же Программисты и не просто не «рисуем» программы, а часто управляем, что там скрывать, в том числе и именно самолетами!
Да, чуть не забыл. Автоматное программирование (АП) я бы классифицировал как программирование с динамическим управлением. Насчет асинхронности — поспорю. С учетом, что в основе модели управления АП сеть в едином времени, т.е. синхронные сети автоматов, то оно синхронное. Но поскольку среда ВКПа реализует также множество сетей через понятие «автоматных миров», то оно вполне и асинхронное. В целом же я против каких-либо очень жестких рамок классификаций, но и не за анархию. В этом смысле в ВКПа, надеюсь, достигнут определенный компромисс между жесткостью последовательно-параллельного программирования и определенным анархизмом асинхронного. С учетом того, что автоматное программирование покрывает еще и класс событийных программ (см. [4]), а потоковые программы легко в его рамках моделируются, то о каком программировании еще можно мечтать? Мне то уж — точно.
Литература
1. Элементы параллельного программирования/В.А. Вальковский, В.Е. Котов, А.Г. Марчук, Н.Н. Миренков; Под ред. В.Е. Котова. – М.: Радио и связь, 1983. – 240с.
2. Ментальные Модели Реактивного Программирования для начальников. [Электронный ресурс], Режим доступа: habr.com/ru/post/486632 свободный. Яз. рус. (дата обращения 07.02.2020).
3. Реактивное программирование. ВикипедиЯ. [Электронный ресурс], Режим доступа: ru.wikipedia.org/wiki/Реактивное_программирование свободный. Яз. рус. (дата обращения 07.02.2020).
4. Автомат — вещь событийная? [Электронный ресурс], Режим доступа: habr.com/ru/post/483610 свободный. Яз. рус. (дата обращения 07.02.2020).
2. Ментальные Модели Реактивного Программирования для начальников. [Электронный ресурс], Режим доступа: habr.com/ru/post/486632 свободный. Яз. рус. (дата обращения 07.02.2020).
3. Реактивное программирование. ВикипедиЯ. [Электронный ресурс], Режим доступа: ru.wikipedia.org/wiki/Реактивное_программирование свободный. Яз. рус. (дата обращения 07.02.2020).
4. Автомат — вещь событийная? [Электронный ресурс], Режим доступа: habr.com/ru/post/483610 свободный. Яз. рус. (дата обращения 07.02.2020).