Sur le corutinisme concurrentiel (en utilisant la programmation réactive comme exemple)

1. Introduction


La compétition pour les esprits, les humeurs et les aspirations des programmeurs est, il me semble, une tendance moderne dans le développement de la programmation. Quand presque rien n'est proposé, bien que sous le slogan de la lutte pour cela. Il est très, très difficile de reconnaître dans l'écrasement des paradigmes logiciels quelque chose de nouveau, qui s'avère en fait souvent assez connu et parfois simplement dépassé. Tout est «emporté» par les délices terminologiques, l'analyse détaillée et les exemples multilignes dans de nombreux langages de programmation. Dans le même temps, les demandes d'ouverture et / ou de réflexion sur le fond de la solution, l'essence des innovations sont obstinément évitées, les tentatives de savoir combien cela est nécessaire et ce qui en fin de compte, qui distingue qualitativement l'innovation des approches et des outils de programmation déjà connus, sont contrecarrées à l'œuf.

Je suis apparu sur Habré, comme on l'a bien vu dans l'une des discussions, après un certain gel. Ça ne me dérange même pas. Du moins, l'impression, apparemment, n'est que cela. Par conséquent, je suis d'accord, je l'avoue, même si, si c'est ma faute, ce n'est que partiellement. J'avoue, je vis des idées sur la programmation parallèle, formées dans les années 80 du siècle dernier. Antiquité? Peut être. Mais dites-moi ce qu'il y a de nouveau, à propos duquel la science de la programmation [parallèle] ne serait pas déjà connue à l'époque (voir détails [1]). À cette époque, les programmes parallèles étaient divisés en deux classes - parallèle-série et asynchrone. Si les premiers étaient déjà considérés comme archaïques, alors les seconds - avancés et vraiment parallèles. Parmi ces derniers, la programmation avec contrôle des événements (ou simplement la programmation des événements), le contrôle des flux et la programmation dynamique ont été distingués.C'est tout en général. Plus de détails déjà.

Et qu'offre la programmation actuelle en plus de ce qui est déjà connu il y a au moins 40 ans? Dans mon "regard gelé" - rien. Les coroutines, comme il s'est avéré, sont maintenant appelées coroutines ou même goroutines; les termes concurrence et concurrence entrent dans une stupeur, semble-t-il, pas seulement des traducteurs. Et il n'y a pas de tels exemples. Par exemple, quelle est la différence entre la programmation réactive (RP) et la programmation d'événements ou le streaming? À laquelle des catégories et / ou classifications connues appartient-il? Personne ne semble intéressé par cela, et personne ne peut le clarifier. Ou pouvez-vous maintenant classer par nom? Alors, en effet, les coroutines et les coroutines sont des choses différentes, et la programmation parallèle est simplement obligée de différer de la concurrence. Et les machines d'état? De quel genre de technique miracle s'agit-il?

Les «spaghettis» dans la tête proviennent de l'oubli de la théorie où, lorsqu'un nouveau modèle est introduit, il est comparé à des modèles déjà connus et bien étudiés. Si cela se fera bien, mais au moins vous pouvez le découvrir, car le processus est formalisé. Mais comment aller au fond des choses si vous donnez un nouveau surnom aux coroutines puis choisissez le «code de capot moteur» simultanément en cinq langues, évaluant en outre la perspective de migration vers les streams. Et ce ne sont que des coroutines, qui, franchement, devraient déjà être oubliées en raison de leur nature élémentaire et de leur faible utilisation (il s'agit bien sûr de mon expérience).

2. Programmation réactive et tout, tout, tout


Nous ne nous fixerons pas l'objectif de bien comprendre le concept de «programmation réactive», même si nous prendrons «l'exemple réactif» comme base pour une discussion plus approfondie. Son modèle formel sera créé sur la base du modèle formel bien connu. Et cela, je l'espère, nous permettra de comprendre clairement, avec précision et en détail l'interprétation et le fonctionnement du programme original. Mais dans quelle mesure le modèle créé et sa mise en œuvre seront «réactifs», c'est aux apologistes de ce type de programmation de décider. Pour le moment, il suffira pour l'instant que le nouveau modèle devra implémenter / modéliser toutes les nuances de l'exemple d'origine. Si quelque chose n'est pas pris en compte, alors j'espère qu'il y a ceux qui me corrigent.

Ainsi, dans [2], un exemple de programme réactif a été considéré, dont le code est montré dans le Listing 1.

Listing 1. Code de programme réactif
1. 1 = 2 
2. 2 = 3 
3. 3 = 1 + 2 
4.  1, 2, 3 
5. 1 = 4 
6.  1, 2, 3


Dans le monde de la programmation réactive, le résultat de son travail sera différent du résultat d'un programme régulier du même genre. Cela seul est mauvais, pour ne pas dire laideur, car Le résultat du programme doit être sans ambiguïté et ne pas dépendre de la mise en œuvre. Mais plus confond l'autre. Premièrement, en apparence, il n'est guère possible de distinguer un code similaire régulier d'un code réactif. Deuxièmement, apparemment, l'auteur lui-même n'est pas entièrement sûr du travail du programme réactif, parlant du résultat «très probablement». Et troisièmement, lequel des résultats est considéré comme correct?

Une telle ambiguïté dans l'interprétation du code a conduit au fait qu'il n'est pas immédiatement possible de le «couper». Mais alors, comme cela arrive souvent, tout s'est avéré être beaucoup plus simple que ce à quoi on aurait pu s'attendre. La figure 1 montre deux diagrammes structurels qui, espérons-le, correspondent à la structure et expliquent le fonctionnement de l'exemple. Dans le diagramme supérieur, les blocs X1 et X2 organisent la saisie des données, signalant au bloc X3 leur modification. Ce dernier effectue la sommation et permet au bloc Pr d'imprimer les valeurs actuelles des variables. Après avoir imprimé, le bloc Pr signale au bloc X3, d'ailleurs, à lui et seulement à lui qu'il est prêt à imprimer de nouvelles valeurs.

Figure. 1. Deux modèles structurels de l'exemple
image

Le deuxième schéma, par rapport au premier, est assez élémentaire. Dans le cadre d'un seul bloc, il entre des données et implémente séquentiellement: 1) le calcul de la somme des données d'entrée et 2) leur impression. Le remplissage interne du bloc à ce niveau de présentation n'est pas divulgué. Bien que l'on puisse dire qu'au niveau structurel, il peut s'agir d'une «boîte noire comprenant un schéma à quatre blocs. Mais encore, son appareil [algorithmique] est censé être différent.

Commentaire. L'approche du programme comme une boîte noire reflète essentiellement l'attitude de l'utilisateur à son égard. Ce dernier ne s'intéresse pas à sa mise en œuvre, mais au résultat des travaux. Qu'il s'agisse d'un programme réactif, d'un programme événementiel ou d'un autre, mais le résultat conforme à la théorie des algorithmes doit être sans ambiguïté et prévisible.

En figue. 2 présente des modèles algorithmiques qui clarifient en détail la structure interne [algorithmique] des blocs de circuits. Le modèle supérieur est représenté par un réseau d'automates, où chacun des automates est un modèle algorithmique d'un bloc séparé. Les connexions entre les automates représentées par des arcs en pointillés correspondent aux connexions du circuit. Un modèle à un seul automate décrit l'algorithme de fonctionnement d'un schéma fonctionnel composé d'un bloc (voir un bloc Pr séparé sur la figure 1).

Figure. 2. Modèles algorithmiques pour les schémas structurels
image

Les automates X1 et X2 (les noms des automates et des blocs coïncident avec les noms de leurs variables), détectent les changements et, si l'automate X3 est prêt à effectuer l'opération d'addition (dans l'état "s0"), passez dans l'état "s1" en se souvenant de la valeur actuelle de la variable. La machine X3, ayant reçu l'autorisation d'entrer dans l'état "s1", effectue l'opération d'addition et, si nécessaire, attend la fin de l'impression des variables. «Machine d'impression» Pr, ayant terminé l'impression, revient à l'état initial «p0», où il attend la commande suivante. Notez que son état "p1" démarre une chaîne de transitions inverses - l'automate X3 à l'état "s0", et X1 et X2 à l'état "s0". Après cela, l'analyse des données d'entrée, puis leur sommation et leur impression ultérieure sont répétées.

Comparé au réseau d'automates, l'algorithme d'un automate Pr séparé est assez simple, mais, notons-le, fait le même travail et peut-être même plus rapidement. Ses prédicats révèlent un changement de variables. Dans ce cas, le passage à l'état «p1» est effectué au début de l'action y1 (voir Fig. 2), qui résume les valeurs actuelles des variables, tout en les mémorisant. Ensuite, lors d'une transition inconditionnelle de l'état "p1" à l'état "p0", l'action y2 imprime les variables. Après cela, le processus revient à l'analyse des données d'entrée. Le code d'implémentation du dernier modèle est indiqué dans le Listing 2.

Listing 2. Implémentation de l'automate 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);
}


La quantité de code est clairement incomparablement plus grande que l'exemple d'origine. Mais, notez, pas un seul code. La nouvelle solution supprime tous les problèmes de fonctionnement, ne permettant pas de se heurter à des fantasmes dans l'interprétation du programme. Un exemple qui semble compact et élégant, mais dont vous pouvez dire «très probablement», ne provoque pas, disons, d'émotions positives et le désir de travailler avec. Il faut aussi noter qu'il faut comparer effectivement avec l'action de l'automate y1.

Le reste du code est lié aux exigences de "l'environnement automatique", qui, je le note, n'est pas énoncé dans le code source. Ainsi, la méthode FCreationOfLinksForVariables de la classe d'automate de base LFsaApplcrée des variables locales pour la machine et les relie à celles-ci lorsque, au niveau de l'environnement VKPA, les noms symboliques des autres variables d'environnement qui leur sont associées sont indiqués. La première fois qu'il démarre lors de la création d'un automate, puis dans le cadre de la méthode FInit (voir étape y12), car tous les liens ne sont pas connus lors de la création d'un objet. La machine sera dans l'état "st" jusqu'à ce que tous les liens nécessaires que les vérifications de prédicat x12 soient initialisés. Une référence à une variable, si son nom lui est renvoyé, renvoie la méthode GetAddressVar.

Pour supprimer d'éventuelles questions, nous vous présentons le code du réseau d'automates. Il est montré dans le Listing 3 et inclut le code de trois classes d'automates. C'est sur leur base que de nombreux objets sont créés qui correspondent au schéma structurel du réseau représenté sur la Fig. 1. Notez que les objets X1 et X2 sont dérivés de la classe générale FSynch.

Listing 3. Classes de réseau automatisées
#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);
}


Ce code est différent du Listing 1, comme une photo d'un avion de sa documentation de conception. Mais, je pense, nous sommes principalement des programmeurs, et, aucune infraction ne leur sera dite, certains designers. Notre «code de conception» doit être facile à comprendre et à interpréter sans ambiguïté afin que notre «avion» ne s'écrase pas lors du premier vol. Et si un tel malheur arrivait, et avec les programmes, cela se produit plus souvent qu'avec les avions, alors la raison peut être trouvée facilement et rapidement.

Par conséquent, en considérant le Listing 3, vous devez imaginer que le nombre de classes n'est pas directement lié au nombre d'objets correspondants dans le programme parallèle. Le code ne reflète pas la relation entre les objets, mais contient les mécanismes qui les créent. Ainsi, la classe FSynch contient un pointeur pL vers un objet de typeLFsaAppl . Le nom de cet objet est déterminé par une variable locale, qui dans l'environnement VKPa correspondra à une variable automate avec le nom strNameObject . Un pointeur est nécessaire pour utiliser la méthode FGetState pour surveiller l'état actuel d'un objet automate de type FSynch (voir le code de prédicat x2). Des pointeurs similaires aux objets, des variables pour spécifier les noms des objets et des prédicats nécessaires à l'organisation des relations contiennent d'autres classes.

Maintenant, quelques mots sur la «construction» d'un programme parallèle dans l'environnement VKPA. Il est créé lors du chargement de la configuration du programme. Dans ce cas, les premiers objets sont créés à partir de classes issues de bibliothèques thématiques dynamiques de type automate (leur ensemble est déterminé par la configuration de l'application / du programme). Les objets créés sont identifiés par leurs noms (appelons-les variables automatiques) Ensuite, les valeurs nécessaires sont écrites dans les variables locales des automates. Dans notre cas, les variables avec un type de chaîne sont définies sur les noms de variables d'autres objets et / ou les noms des objets. De cette façon, les connexions entre les objets d'un programme d'automate parallèle sont établies (voir Fig. 1). De plus, en modifiant les valeurs des variables d'entrée (en utilisant des boîtes de dialogue de contrôle d'objet individuelles ou les boîtes de dialogue standard / environnement pour définir les valeurs des variables d'environnement), nous fixons le résultat. Il peut être vu à l'aide d'une boîte de dialogue d'environnement standard pour afficher les valeurs des variables.

3. À l'analyse des programmes parallèles


Concernant le fonctionnement d'un programme parallèle, à moins qu'il ne soit assez simple séquentiellement parallèle, il est très, très difficile de dire quelque chose de concret. Le réseau d'automates considéré ne fait pas exception. Ensuite, nous verrons cela, en comprenant ce que l'on peut en attendre.

L'automate résultant et le réseau pour lequel il est construit sont illustrés à la Fig. 3. Depuis le réseau de la Fig. 2, en plus de renommer ses éléments - automates, signaux d'entrée et de sortie, il se distingue par l'absence d'une «machine d'impression» de variables. Ce dernier n'est pas indispensable pour le fonctionnement du réseau, et le renommage vous permet d'utiliser l'opération de composition pour construire l'automate résultant. De plus, pour créer des noms plus courts, un codage a été introduit lorsque, par exemple, l'état "a0" de l'automate A est représenté par le symbole "0", et "a1" par le symbole "1". De même pour les autres machines. Dans ce cas, l'état des composants du réseau, par exemple «a1b0c1», est affecté du nom «101». De même, des noms sont formés pour tous les états des composants du réseau, dont le nombre est déterminé par le produit des états des automates des composants.

Figure. 3. L'automate réseau résultant
image

L'automate résultant peut, bien sûr, être calculé de manière purement formelle, mais pour cela, nous avons besoin d'une «calculatrice» appropriée. Mais si ce n'est pas le cas, vous pouvez utiliser un algorithme intuitif assez simple. Dans son cadre, l'un ou l'autre état des composants du réseau est enregistré puis, en triant toutes les situations d'entrée possibles, les états des composants cibles sont déterminés par des «poignées». Ainsi, après avoir fixé l'état "000" correspondant aux états actuels des automates composants - "a0", "b0", "c0", les transitions pour les conjonctions des variables d'entrée ^ x1 ^ x2, ^ x1x2, x1 ^ x2, x1x2 sont déterminées. Nous obtenons les transitions respectivement dans indique "a0b0c0", "a0b1c0", "a1b0c0", "a1b1c0", qui sont marqués "000", "010", "100" et "110" sur la machine résultante. bouclesqui ne sont pas chargés d'actions peuvent être exclus du graphique.

Ce que nous avons "dans le résidu sec". Nous avons atteint l'essentiel - nous avons reçu l'automate résultant, qui décrit avec précision le fonctionnement du réseau. Nous avons découvert que sur huit états de réseau possibles, un est inaccessible (isolé) - état «001». Cela signifie que l'opération de sommation ne sera en aucun cas déclenchée pour les variables d'entrée qui n'ont pas modifié la valeur actuelle.

Ce qui est inquiétant, même si les tests n'ont pas révélé d'erreurs. Sur le graphique de l'automate résultant, des transitions conflictuelles dans les actions de sortie ont été trouvées. Ils sont marqués par une combinaison des actions y1y3 et y2y3. Les actions y1 et y2 sont déclenchées lorsque les données d'entrée changent, puis une autre action y3 calcule la somme des variables en parallèle avec elles. Sur quelles valeurs fonctionnera-t-elle - ancienne ou simplement changée par de nouvelles? Pour éliminer l'ambiguïté, vous pouvez simplement modifier les actions de y3 et y4. Dans ce cas, leur code sera le suivant: X3 = X1Sav + X2Sav et imprimer (X1Sav, X2Sav, X3).

Donc. La construction de l'automate résultant a révélé des problèmes évidents dans le modèle parallèle créé. Qu'ils apparaissent dans le programme réactif est une question. Tout dépendra, apparemment, de l'approche de la mise en œuvre du parallélisme dans le paradigme réactif. Dans tous les cas, une telle dépendance doit être prise en compte et en quelque sorte éliminée. Dans le cas d'un réseau automatisé, il est plus facile de quitter la version modifiée que d'essayer de changer le réseau. Ce n'est pas grave si les «anciennes» données qui ont déclenché le fonctionnement du réseau sont imprimées en premier, puis les données actuelles sont imprimées ensuite.

4. Conclusions


Chacune des solutions envisagées a ses avantages et ses inconvénients. Le premier est très simple, le réseau est plus compliqué, et créé à partir d'une seule machine, il ne commencera à analyser les données d'entrée qu'après sa visualisation. En raison de son parallélisme, le même réseau automatique commencera l'analyse des données d'entrée avant la fin de la procédure d'impression. Et si le temps de visualisation est long, mais ce sera le cas contre l'opération de sommation, alors le réseau sera plus rapide du point de vue du contrôle d'entrée. Ceux. une évaluation basée sur une estimation de la quantité de code dans le cas de programmes parallèles n'est pas toujours objective. En termes plus simples, le réseau est parallèle, la solution à un composant est largement séquentielle (ses prédicats et ses actions sont parallèles). Et nous parlons tout d'abord de programmes parallèles.

Le modèle de réseau est également un exemple de solution flexible. Premièrement, les composants peuvent être conçus indépendamment les uns des autres. Deuxièmement, tout composant peut être remplacé par un autre. Et troisièmement, tout composant réseau peut être un élément d'une bibliothèque de processus automatiques et est utilisé dans une autre solution réseau. Et ce ne sont que les avantages les plus évidents d'une solution parallèle.

Mais revenons à une programmation réactive. RP considère-t-il que toutes les instructions de programme sont initialement parallèles? On peut seulement supposer que sans cela, il est difficile de parler d'un paradigme de programmation «orienté vers les flux de données et la propagation des changements» (voir la définition de la programmation réactive dans [3]). Mais alors quelle est sa différence par rapport à la programmation avec contrôle de streaming (pour plus de détails, voir [1])? Nous revenons donc à notre point de départ: comment classer la programmation réactive dans le cadre de classifications bien connues? Et, si RP est une programmation spéciale, qu'est-ce qui est différent des paradigmes de programmation connus?

Eh bien, sur la théorie. Sans cela, l'analyse d'algorithmes parallèles ne serait pas seulement difficile, voire impossible. Dans le processus d'analyse, des problèmes sont parfois identifiés qui, même avec un regard attentif et réfléchi sur le programme, comme, accessoirement, sur le «document de conception», sont impossibles à deviner. En tout cas, je suis pour le fait que les avions, au figuré comme dans tout autre sens, ne plantent pas. C'est moi au fait que, bien sûr, vous devez viser la simplicité et la grâce de la forme, mais sans perte de qualité. Nous, programmeurs, ne nous contentons pas de «dessiner» des programmes, mais nous contrôlons souvent ce qui y est caché, y compris par les avions!

Oui, j'ai presque oublié. Je classerais la programmation automatique (AP) comme une programmation avec contrôle dynamique. Quant à l'asynchronie - je parie. Étant donné que la base du modèle de contrôle AP est un réseau en un seul instant, c'est-à-dire réseaux synchrones d'automates, alors il est synchrone. Mais puisque l'environnement VKPa implémente également de nombreux réseaux à travers le concept de «mondes d'automates», il est complètement asynchrone. En général, je suis contre tout cadre de classification très rigide, mais pas pour l'anarchie. En ce sens, en VKPa, j'espère qu'un certain compromis a été trouvé entre la rigidité de la programmation série-parallèle et un certain anarchisme asynchrone. Étant donné que la programmation automatique couvre également la classe des programmes d'événements (voir [4]) et que les programmes de flux y sont facilement modélisés,de quelle programmation pouvez-vous encore rêver? Bien sûr - pour moi.

Littérature
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).

Source: https://habr.com/ru/post/undefined/


All Articles