A world without coroutines. Crutches for the programmer - asyncio

1. Introduction



The one who has learned to fly will no longer crawl. But there should also be no arrogance towards someone who "cannot fly" in principle. Both are quite normal. Both are respected and honorable. For a person, this is like choosing a profession: you, conventionally, are either a pilot or a driver. For the same animals, it is the same - you are either an eagle or a wolf, i.e. Either you fly or you run (run away) But only man in his concepts, categories, attitudes and thoughts endowed the characters with characteristics and developed his own attitude towards them. True, with nuances. So, no, probably, it is more honorable and romantic than the profession of a pilot, but try to convince a trucker or aircraft designer of this ?! And here it is difficult to argue: there are a lot of astronauts even now, and there is still no second Queen!



We are programmers. Maybe to varying degrees, but some - for sure. This I mean that we are different and we can think in different ways too. The statement that a programmer only thinks consistently is just as one-sided, harmful and even blasphemous, as the fact that a person only runs. He sometimes - and flies. Some, like pilots, do it quite regularly, and some, like astronauts, even for months and continuously. The idea of ​​consistent thinking diminishes human ability. At some point and for a while, you can even believe it, but "still it turns" - this is about the fact that sooner or later life will take its toll.



Asyncio in Python is a software crutch that mimics, figuratively speaking, the flight of incorrect parallel thinking. A kind of bouncing with waving hands. It looks, at times, funny and clumsy. Although in a certain situation this is also a way out: you can just cross the puddle and get dirty, but if the strength allows, then it is better to jump over. But maybe the programmers lack the strength?



Let's try to discard the imposed "software crutches" and soar above the software routine. And let it be not a jump, or maybe not so high and long, but still, especially in comparison with crutches, a flight. After all, once Alexander Fyodorovich Mozhaisky (not to be confused with the Mozhaisky City Court of the Moscow Region;) or the same Wright brothers overcame several hundred meters in the air for the first time. Yes, and tests of modern aircraft begin with a run and a short-term separation from the runway.



2. A very simple example with asyncio



We'll start with flying in Python. The flight program is simple. There are planes (which, however, in the original version of the imagery of spiders, see [1] ) with names on the fuselages "Blog", "News", "Forum". They take off at the same time. Everyone must fly a segment of the path in a certain time and throw, say, a flag with the number of the segment covered. This must be done three times. And only then land.



In Python, a model of this behavior is described and then simulated by the code in Listing 1.



Listing 1. Python code for spider planes
import asyncio
import time

async def spider(site_name):
 for page in range(1, 4):
     await asyncio.sleep(1)
     print(site_name, page)

spiders = [
 asyncio.ensure_future(spider("Blog")),
 asyncio.ensure_future(spider("News")),
 asyncio.ensure_future(spider("Forum"))
]

start = time.time()

event_loop = asyncio.get_event_loop()
event_loop.run_until_complete(asyncio.gather(*spiders))
event_loop.close()

print("{:.2F}".format(time.time() - start))




The simulation results for such a "flight" are as follows:



Blog 1

News 1

Forum 1

Blog 2

News 2

Forum 2

Blog 3

News 3

Forum 3

3.00



Why this is explained in such detail by the video [1]. But we guys with imagination and the simultaneous (according to the scenario - asynchronous) flight of our three "planes" without using asyncio will present in a different way - on the basis of automatic models. For example, Listing 2 shows the code for an automaton delay, an analog of the async delay from the asyncio module, represented by the line await asyncio.sleep (1) in Listing 1.



Listing 2. Automatic delay code in Python
import time

class PSleep:
    def __init__(self, t, p_FSM): self.SetTime = t; self.nState = 0; self.bIfLoop = False; self.p_mainFSM = p_FSM
    def x1(self): return time.time() - self.t0 <= self.SetTime
    def y1(self): self.t0 = time.time()
    def loop(self):
        if (self.nState == 0): self.y1(); self.nState = 1
        elif (self.nState == 1):
            if (not self.x1()): self.nState = 4




The delay value and a pointer to the object that created the delay object are passed through the class constructor. The pointer is required by the process control function, which, after removing the delay, will continue the parent process, which was stopped when it was created.



Listing 3 shows an automaton counterpart to the asynchronous spider plane (see also Listing 1). It is very likely that a Python programming ace will not dream of this, even in the most nightmare! The source code of four lines has grown 15 times! Is this not a reason for admiration for typical Python code in general and asycio in particular, or, at least, a proof of the advantage of "coroutine technology" over automaton programming?



Listing 3. Code for an automaton spider in Python
# ""   "Blog"
class PBSpider:
    def __init__(self, name):
        self.nState = 0; self.bIfLoop = True; self.site_name = name; self.page = 1;
        self.p_mainFSM = b_sleep;
    def x1(self): return self.page < 4
    def y1(self):
        self.bIfLoop = False; automaton.append(b_sleep);
        b_sleep.p_mainFSM = blog
        automaton[-1].bIfLoop = True;
        automaton[-1].nState = 0
    def y2(self): print(self.site_name, self.page)
    def y3(self): self.page += 1
    def y4(self): self.page = 1
    def loop(self):
        if (self.x1() and self.nState == 0):  self.y1(); self.nState = 1
        elif (not self.x1()  and self.nState == 0): self.y1(); self.y4(); self.nState = 33
        elif (self.nState == 1): self.y2(); self.y3(); self.nState = 0

# ""   "News"
class PNSpider:
    def __init__(self, name):
        self.nState = 0; self.bIfLoop = True; self.site_name = name; self.page = 1;
        self.p_mainFSM = n_sleep;
    def x1(self): return self.page < 4
    def y1(self):
        self.bIfLoop = False; automaton.append(n_sleep);
        n_sleep.p_mainFSM = news
        automaton[-1].bIfLoop = True;
        automaton[-1].nState = 0
    def y2(self): print(self.site_name, self.page)
    def y3(self): self.page += 1
    def y4(self): self.page = 1
    def loop(self):
        if (self.x1() and self.nState == 0):  self.y1(); self.nState = 1
        elif (not self.x1()  and self.nState == 0): self.y1(); self.y4(); self.nState = 33
        elif (self.nState == 1): self.y2(); self.y3(); self.nState = 0

#    "Forum"
class PFSpider:
    def __init__(self, name):
        self.nState = 0; self.bIfLoop = True; self.site_name = name; self.page = 1;
        self.p_mainFSM = f_sleep;
    def x1(self): return self.page < 4
    def y1(self):
        self.bIfLoop = False; automaton.append(f_sleep);
        f_sleep.p_mainFSM = forum
        automaton[-1].bIfLoop = True;
        automaton[-1].nState = 0
    def y2(self): print(self.site_name, self.page)
    def y3(self): self.page += 1
    def y4(self): self.page = 1
    def loop(self):
        if (self.x1() and self.nState == 0):  self.y1(); self.nState = 1
        elif (not self.x1()  and self.nState == 0): self.y1(); self.y4(); self.nState = 33
        elif (self.nState == 1): self.y2(); self.y3(); self.nState = 0

# 
b_sleep = PSleep(1, 0)
n_sleep = PSleep(1, 0)
f_sleep = PSleep(1, 0)
# ""
blog = PBSpider("Blog")
news = PNSpider("News")
forum = PFSpider("Forum")
#    
automaton = []
automaton.append(blog);
automaton.append(news);
automaton.append(forum);
start = time.time()
#   ( event_loop)
while True:
    ind = 0;
    while True:
        while ind < len(automaton):
            if automaton[ind].nState == 4:
                automaton[ind].p_mainFSM.bIfLoop = True
                automaton.pop(ind)
                ind -=1
            elif automaton[ind].bIfLoop:
                automaton[ind].loop()
            elif automaton[ind].nState == 33:
                print("{:.2F}".format(time.time() - start))
                exit()
            ind += 1
        ind = 0




And here is the result of automatic flights:



News 1

Forum 1

Blog 1

Blog 2

News 2

Forum 2

News 3

Forum 3

Blog 3

3.00



But - let's discuss. The increase in code size was due to problems with pointers in Python. As a result, I had to create a class for each page, which tripled the code. Therefore, it is more correct to speak not about 15, but about a fivefold increase in volume. A more skillful "Python pilot" in programming may even be able to eliminate this shortcoming.



But the main reason is still not pointers. The C ++ code shown below, with complete freedom to work with pointers, has even more lines per class. The reason is the used computational model, the language of its description and approaches to the implementation of algorithms based on it. Figure: 1 shows a conventional spider plane model in block diagram form and a machine gun model. You can see that outwardly and in quality, these are different models, although they allow equivalent transformations. Automata have states, but block diagrams don't even have a trace of them. Automata, by definition, operate in discrete time, and block diagrams do not even dream of this. All this imposes certain obligations on the implementation of the model.



The absence of the concept of discrete time is the essence of the problems of the existing block-diagram programming model, which, strictly speaking, must realize what is unrealizable for it, i.e. parallel processes. Recall that for automata an automaton network, as a model of parallel processes (as well as asynchronous), is their natural state.



Figure: 1. Automatic and block-diagram models of a spider plane
image



But even at the level of a separate process, models have differences that are projected onto the language and implementation of the model. By virtue of these qualities, the apologists for a consistent block diagram have created language constructs that, either explicitly or implicitly, make it possible to describe it very compactly. Take the same for loop or at least an implicitly implied sequence of operator execution (actions y1, y2, y3).



For a flowchart, you can list the actions in one box without any problems and this will not change the sequential nature of their work. If the automaton replaces the transitions in the states s2, s3 with a cycle in the state s1, marking the arc with the same actions, then the meaning of the algorithm will change, since will introduce parallelism to its work. Since the above actions must be performed strictly sequentially, this predetermined the appearance of the automaton model (see Fig. 1). A finite automaton is a model that does not allow "doublethink".



The lack of discrete time in block diagrams was their advantage. But now this has become their main disadvantage. It affects, as it may not seem blasphemous, and the way of thinking. Sequential languages ​​justify the sequential thinking of programmers by denying them something else - parallel. This is what justifies the constructions of the existing asynchronous programming, presenting the set and functionality of operators of the same asyncio package. And it is precisely this approach, it is argued, that allows programmers familiar with sequential programs to turn into asynchronous (almost parallel).



But back to the topic of the article and its images. We wanted our "plane" and we got it! Flights, or, more precisely, their visible results, are somewhat different in appearance, but completely indistinguishable in nature. They can be interpreted in such a way that the flags were selected and recorded in the protocol in a different order, but the "planes" themselves flew as they should, i.e. simultaneously and simultaneously threw out their flags. And in what order they were recorded - the case, as they say, is the tenth. The main thing is realized and completed: the sequence and times of their dropping correspond to the flight program



The code can be shortened. So, apparently, you can limit yourself to the code of only one class. You can also hide the event loop code. If, at the same time, in the source code, you open the engine compartment code, which is hidden behind the asyncio and await operators, then the volume of the automaton code will most likely not be so scary.



3. On the problems of implementing automata in Python



Let us dwell in more detail on the problems that gave rise to the appearance of the automatic code. The last thing to hide there looks so far monstrous in comparison with the source code. But let us note that the first aircraft of Mozhaisky was far from similar to the modern "drying", and the first cars did not differ for the better from any of their modern counterparts. Let me emphasize that the problems of the presented automaton code are largely related to my current understanding of the Python language and, possibly, to a lesser extent with the capabilities of the language itself.



Nevertheless, the first problem is related to the language of description of the automaton model. In C ++ it is solved by means of the language. I don’t see such possibilities in Python. Unfortunately, as they sometimes say now, from the word at all. Therefore, the method of implementing automata based on control operators of the if-elif-else language was taken as a basis. In addition, we recall that in the CPSU (a), in addition to the automata themselves, shadow memory and automata spaces were introduced to fully implement parallelism. Without this, the possibilities of automaton programming are very limited and in many ways inferior.



The next problem we already mentioned is pointers. There are no problems with them in C ++. Within the framework of the CPSU (a), in accordance with the OOP paradigm, a basic automaton class has been created, from which applied automata classes are generated, and their parameters can be not only pointers, but even their addresses. All this makes it possible to simply, compactly and very efficiently describe and implement any task that includes many parallel interacting processes.



The following is the code of C ++ automata classes equivalent to the considered example. The delay code in Listing 4 is equivalent to the line await asyncio.sleep (1) in Listing 1. In graphical form, it corresponds to the FAwaitSleep automaton model in Figure 1. 1. Such and only such an automaton can be considered asynchronous and it will not slow down the computational flow. FSleep in the same figure corresponds to the usual sleep () operator. It is simpler, but guaranteed to destroy the discrete time model due to the action of y1 causing the usual sequential delay. And this is no longer good for anything.



Listing 4. Asynchronous delay code
//  (  )
#include "lfsaappl.h"
#include <QTime>

class FAwaitSleep :
    public LFsaAppl
{
public:
    FAwaitSleep(int n);
protected:
    int x1();
    QTime time;
    int nAwaitSleep;
};

#include "stdafx.h"
#include "FAwaitSleep.h"

static LArc TBL_AwaitSleep[] = {
    LArc("s1",		"s1","x1",  "--"),			//
    LArc("s1",		"00","^x1",	"--"),			//
    LArc()
};

FAwaitSleep::FAwaitSleep(int n):
    LFsaAppl(TBL_AwaitSleep, "FAwaitSleep")
{
    nAwaitSleep = n; time.start();
}

int FAwaitSleep::x1() { return time.elapsed() < nAwaitSleep; }




The C ++ code for the spider-plane is shown in Listing 5. This code is much more adequate to its model than a block diagram of the Python code. Especially if we compare the transition table of the automaton and the appearance of the automaton graph. They are simply different forms of describing the same abstract concept - an automaton. It also shows how a pointer to the parent class is passed when creating a delay (see the call to the FCall method in activity y1)



Listing 5. Code for a spider plane that simulates reading site pages
// "".   
#include "lfsaappl.h"

class FAwaitSleep;
class FSpider :
    public LFsaAppl
{
public:
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FSpider(nameFsa); }
    bool FCreationOfLinksForVariables() override;
    FSpider(string strNam);
    virtual ~FSpider(void);
    CVar *pVarStrSiteName;		//  
    FAwaitSleep *pFAwaitSleep{nullptr};
protected:
    int x1(); void y1(); void y2(); void y3(); void y4();
    int page{1};
};

#include "stdafx.h"
#include "FSpider.h"
#include "FSleep.h"
#include "FAwaitSleep.h"
#include <QDebug>

static LArc TBL_Spider[] = {
    LArc("st","s1","--","--"),		
    LArc("s1","s2","x1","y1"),  // x1- <. ; y1-;
    LArc("s2","s3","--","y2"),  // y2-   ;
    LArc("s3","s1","--","y3"),  // y3-   
    LArc("s1","st","^x1","y4"), // y4-   
    LArc()
};
FSpider::FSpider(string strNam):
    LFsaAppl(TBL_Spider, strNam)
{ }
FSpider::~FSpider(void) { if (pFAwaitSleep) delete pFAwaitSleep; }

bool FSpider::FCreationOfLinksForVariables() {
    pVarStrSiteName = CreateLocVar("strSiteName", CLocVar::vtString, "name of site");
    return true;
}
//      ?
int FSpider::x1() { return page < 4; }
// create delay - pure sleep (synchronous function) or await sleep (asynchronous function)
void FSpider::y1() {
    //sleep(1000);
    // await sleep (asynchronous function)
    if (pFAwaitSleep) delete pFAwaitSleep;
    pFAwaitSleep = new FAwaitSleep(1000);
    pFAwaitSleep->FCall(this);
}
void FSpider::y2() {
#ifdef QT_DEBUG
    string str = pVarStrSiteName->strGetDataSrc();
    printf("%s%d", str.c_str(), page);
    qDebug()<<str.c_str()<<page;
#endif
}
void FSpider::y3() { page++; }
void FSpider::y4() { page = 1; }




There is no code that implements the functions of the so-called event loop. There is simply no need for it. its functions are performed by the core of the CPSU (a) environment. It creates objects and manages their parallel execution in discrete time.



4. Conclusions



Brevity is not always the sister of talent, and sometimes it is also a sign of tongue-tiedness. True, it is difficult to distinguish one from the other at once. Python code will often be shorter than C ++ code. But this is typical for simple cases. The more complex the solution, the less this difference will be. In the end, even the complexity of the solution is determined by the capabilities of the model. An automaton model is much more powerful than a block diagram.



Automata and parallelization are, first of all, very effective means of solving problems of complexity, fighting it, and not so much a means of increasing the speed of a program. Since all this is an automaton model, parallelism is difficult to implement in Python, then, despite all its chips, batteries and much more, it is difficult for me to sway in its direction. I would pay more attention to the C ++ environment, and not very justified introduction of the same coroutines into it. This model is temporary and the reason for its implementation is largely forced. What are we going to do with this "crutch" when the problem of choosing a parallel model is solved?



Therefore, sorry, but my preference is still on the C ++ side. And if you consider the area of ​​my professional interests - industrial systems of the so-called "creepy" hard real time, then I have no choice as such. Yes, some kind of environment, some kind of service can be created using Python. It's convenient, it's fast, there are many prototypes, etc. etc. But the core of the solution, its parallel model, the logic of the processes themselves are unambiguously C ++, unambiguously automata. Here automata, of course, are more important and rule. But not coroutines :)



In addition ... Watch the video [2] , paying attention to the implementation of the rocket model. About her, starting from about 12 minutes, and the video narrates. Respect to the lecturer for using the machines :) And for a sweet treat, another solution from [3]... It's in the spirit of asynchronous programming and asyncio. Actually, it all started with this example - the implementation of nested automata in Python. Here the nesting depth is even greater than in the example detailed above. Listing 6 shows the source code and its Python automaton counterpart. In fig. 2 is the automatic tea drinking model, and Listing 7 shows the equivalent C ++ implementation for VKP (a). Compare, analyze, draw conclusions, criticize ...



Listing 6. Reading and drinking tea asynchronously in Python
import asyncio
import time

# # Easy Python. Asyncio  python 3.7 https://www.youtube.com/watch?v=PaY-hiuE5iE
# # 10:10
# async def teatime():
#     await asyncio.sleep(1)
#     print('take a cap of tea')
#     await asyncio.sleep(1)
#
# async def read():
#     print('Reading for 1 hour...')
#     await teatime()
#     print('...reading for 1 hour...')
#
# if __name__ == '__main__':
#     asyncio.run(read())

class PSleep:
    def __init__(self, t, p_FSM): self.SetTime = t; self.nState = 0; self.bIfLoop = False; self.p_mainFSM = p_FSM
    def x1(self): return time.time() - self.t0 <= self.SetTime
    def y1(self): self.t0 = time.time()
    def loop(self):
        if (self.nState == 0): self.y1(); self.nState = 1
        elif (self.nState == 1):
            if (not self.x1()): self.nState = 4

class PTeaTime:
    def __init__(self, p_FSM): self.nState = 0; self.bIfLoop = False; self.p_mainFSM = p_FSM;
    def y1(self): self.bIfLoop = False; automaton.append(sl); automaton[-1].bIfLoop = True; automaton[-1].nState = 0
    def y2(self): print('take a cap of tea')
    def loop(self):
        if (self.nState == 0):  self.y1(); self.nState = 1
        elif (self.nState == 1): self.y2(); self.nState = 2
        elif (self.nState == 2): self.y1(); self.nState = 3
        elif (self.nState == 3): self.nState = 4

class PRead:
    def __init__(self): self.nState = 0; self.bIfLoop = False;
    def y1(self): print('Reading for 1 hour...')
    def y2(self): self.bIfLoop = False; automaton.append(rt); automaton[-1].bIfLoop = True; automaton[-1].nState = 0
    def loop(self):
        if (self.nState == 0): self.y1(); self.nState = 1
        elif (self.nState == 1): self.y2(); self.nState = 2
        elif (self.nState == 2): self.y1(); self.nState = 33; self.bIfLoop = False

read = PRead()
rt = PTeaTime(read)
sl = PSleep(5, rt)
automaton = []
automaton.append(read); automaton[-1].bIfLoop = True
while True:
    ind = 0;
    while True:
        while ind < len(automaton):
            if automaton[ind].nState == 4:
                automaton[ind].p_mainFSM.bIfLoop = True
                automaton.pop(ind)
                ind -=1
            elif automaton[ind].bIfLoop:
                automaton[ind].loop()
            elif automaton[ind].nState == 33:
                exit()
            ind += 1
        ind = 0




Figure: 2. Automatic model of tea drinking
image



Listing 7. Reading and drinking tea asynchronously in C ++
#include "lfsaappl.h"

class FRead :
    public LFsaAppl
{
public:
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF)return new FRead(nameFsa); }
    FRead(string strNam);
    virtual ~FRead(void);
protected:
    void y1(); void y2();  void y3();
    LFsaAppl *pFRealTime{nullptr};
};

#include "stdafx.h"
#include "FRead.h"
#include "FTeaTime.h"
#include <QDebug>

static LArc TBL_Read[] = {
    LArc("s1","s2","--","y1"),	// Reading for 1 hour...
    LArc("s2","s3","--","y2"),	// Call(TeaTime)
    LArc("s3","s4","--","y1"),	// Reading for 1 hour...
    LArc("s4","s5","--","y3"),	// sleep(5)
    LArc("s5","s1","--","--"),	//
    LArc()
};
FRead::FRead(string strNam):
    LFsaAppl(TBL_Read, strNam)
{ }
FRead::~FRead(void) { if (pFRealTime) delete pFRealTime; }

void FRead::y1() {
#ifdef QT_DEBUG
    qDebug()<<"Reading for 1 hour...";
#endif
}
void FRead::y2() {
    if (pFRealTime) delete pFRealTime;
    pFRealTime = new FTeaTime("TeaTime");
    pFRealTime->FCall(this);
}
void FRead::y3() { FCreateDelay(5000); }


#include "lfsaappl.h"

class FTeaTime :
    public LFsaAppl
{
public:
    FTeaTime(string strNam);
protected:
    void y1(); void y2();
};
#include "stdafx.h"
#include "FTeaTime.h"
#include <QDebug>
#include "./LSYSLIB/FDelay.h"

static LArc TBL_TeaTime[] = {
    LArc("s1",	"s2","--","y1"),// sleep(1)
    LArc("s2",	"s3","--","y2"),// take a cap of tea
    LArc("s3",	"s4","--","y1"),// sleep(1)
    LArc("s4",	"00","--","--"),//
    LArc()
};

FTeaTime::FTeaTime(string strNam):
    LFsaAppl(TBL_TeaTime, strNam)
{ }

void FTeaTime::y1() { FCreateDelay(2000); }
void FTeaTime::y2() {
#ifdef QT_DEBUG
    qDebug()<<"take a cap of tea";
#endif
}




PS



Already after writing the article, after reading the translation of the article by Yerain Diaz [4] , I got acquainted with another rather interesting and rather admiring look at coroutines in general and asyncio in particular. Despite this fact and others like it, we will still "go the other way" :) I agree only on one thing with Rob Pike, that "Concurrency Is Not Parallelesm". Competitiveness, one might even say tougher, has nothing to do with parallelism at all. And it's notable that Google Translate translates this phrase as "Parallelism is not parallelism." The man named Google is certainly wrong. But someone convinced him of this? :)



Literature



  1. Shultais Education. 1. . [ ], : www.youtube.com/watch?v=BmOjeVM0w1U&list=PLJcqk6mrJtxCo_KqHV2rM2_a3Z8qoE5Gk, . . . ( 01.08.2020).
  2. Computer Science Center. 9. async / await ( Python). [ ], : www.youtube.com/watch?v=x6JZmBK2I8Y, . . . ( 13.07.2020).
  3. Easy Python. Asyncio python 3.7. [ ], : www.youtube.com/watch?v=PaY-hiuE5iE, . . . ( 01.08.2020).
  4. Yeray Diaz. Asyncio for the practicing python developer. [Electronic resource], Access mode: www.youtube.com/watch?v=PaY-hiuE5iE , free. Language. Russian (date of treatment 08/01/2020).



All Articles