1. Введение
Тот, кто научился летать, ползать уже не будет. Но не должно быть и высокомерия к тому, кто летать не может в принципе. И то и другое вполне норма. И то и другое уважаемо и почетно. Для человека это, как выбор профессии: вы, условно, либо летчик, либо шофер. Для тех же животных аналогично вы либо орел, либо волк, т.е. либо летаете, либо бегаете (убегаете). Но только человек в своих понятиях, категориях, отношении и мыслях наделил персонажи характеристиками и выработал свое отношение к ним. Правда, с нюансами. Так, нет, наверное, почетнее и романтичнее профессии летчика, но попробуйте в этом убедить дальнобойщика или авиаконструктора?! И тут сложно возразить: космонавтов много даже сейчас, а второго Королева все еще нет!
Мы программисты. Может, в разной степени, но некоторые уж точно. Это я к тому, что мы разные и мыслить можем тоже по-разному. Утверждение, что программист мыслит только последовательно, столь же однобоко, вредно и даже кощунственно, как и то, что человек только бегает. Он иногда и летает. Кто-то, как летчики, делает это довольно регулярно, а некоторые, как космонавты, даже месяцами и непрерывно. Идея последовательного мышления принижает способности человека. В какой-то момент и на какое-то время в это можно даже поверить, но " все-таки она вертится" это про то, что рано или поздно жизнь возьмет свое.
Asyncio в Python это программные костыли, имитирующие, образно выражаясь, полет неправильного параллельного мышления. Этакое подпрыгивание со взмахами рук. Выглядит, порой, смешно и коряво. Хотя в определенной ситуации это тоже выход: можно просто перейти лужу и заляпаться, но, если силы позволяют, то лучше уж перепрыгнуть. Но, может, программистам сил не хватает?
Попробуем отбросить навязываемые программные костыли и воспарить над программной обыденностью. И пусть это будет не прыжок, а, может, не такой уж высокий и длительный, но все же, особенно в сравнении с костылями, полет. Ведь когда-то и Можайский Александр Федорович (не путать с Можайским городским судом Московской области ;) или те же братья Райт преодолели по воздуху впервые несколько сот метров. Да и испытания современных самолетов начинают с пробежки и кратковременного отрыва от взлетной полосы.
2. Совсем простой пример с asyncio
А начнем мы с полетов на Python. Программа полетов простая. Есть самолеты (которые, правда, в исходном варианте образности пауки см. [1]) с именами на фюзеляжах Blog, News, Forum. Вылетают они одновременно. Каждый за определенное время должен пролететь отрезок пути и выбросить, предположим, флажок с номером преодоленного отрезка. Так нужно поступить три раза. И только после этого приземлиться.
На языке Python модель подобного поведения описывает, а затем и моделирует, код листинга 1.
import asyncioimport timeasync 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))
Результаты моделирования подобного полета следующие:
Blog 1
News 1
Forum 1
Blog 2
News 2
Forum 2
Blog 3
News 3
Forum 3
3.00
Почему это так подробно разъясняет видео [1]. Но мы-то ребята с фантазией и одновременный (по сценарию асинхронный) полет наших трех самолетов без использования asyncio представим по-другому на базе автоматных моделей. Так, листинг 2 приводит код автоматной задержки аналога асинхронной задержки из модуля asyncio, представленной строкой await asyncio.sleep(1) в листинге 1.
import timeclass 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
Через конструктор класса передается значение задержки и указатель на объект, создавший объект задержки. Указатель требуется функции управления процессами, которая после удалении задержки продолжит работу родительского процесса, который был остановлен при ее создании.
На листинге 3 показан автоматный аналог асинхронного самолета-паука (см. также листинг 1). Весьма вероятно, что ассу программирования на Python подобное не приснится даже в самом кошмарном сне! Исходный код из четырех строчек увеличился в 15 раз! Это ли не повод восхищения типичным кодом Python вообще и asycio в частности или, как минимум, доказательство преимущества корутинной технологии перед автоматным программированием?
# "паук" для страницы "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
А вот и результат автоматных полетов:
News 1
Forum 1
Blog 1
Blog 2
News 2
Forum 2
News 3
Forum 3
Blog 3
3.00
Но обсудим. Увеличение объема кода произошло из-за проблем с указателями в Python. В результате пришлось создать класс для каждой страницы, что и увеличило код в три раза. Поэтому правильнее говорить не о 15-ти, а о пятикратном увеличении объема. Более искусный в программировании Python-летчик этот недостаток, возможно, сможет даже устранить.
Но основная причина все же не в указателях. Код на С++, показанный далее, при полной свободе работы с указателями, имеет на один класс даже большее число строк. Причина в используемой вычислительной модели, языке ее описания и подходов к реализации алгоритмов на ее базе. Рис. 1 демонстрирует обычную модель самолета-паука в форме блок-схемы и модель в форме автомата. Можно видеть, что внешне и по качеству это разные модели, хотя и допускающие эквивалентные преобразования. У автоматов есть состояния, у блок-схем их нет и в помине. Автоматы по определению работают в дискретном времени, а блок-схемы об этом и не мечтают. Все это накладывает на реализацию модели определенные обязательства.
Отсутствие понятия дискретного времени суть проблем существующей блок-схемной модели программирования, которая должна реализовать, строго говоря, для нее нереализуемое, т.е. параллельные процессы. Напомним, что для автоматов автоматная сеть, как модель параллельных процессов (а также асинхронных), это естественное их состояние.
Но даже на уровне отдельного процесса у моделей есть отличия, которые проецируются на язык и реализацию модели. В силу этих качеств апологеты последовательной блок-схемной модели создали конструкции языка, которые в явном или неявном виде позволяют весьма компактно ее описать. Взять тот же цикл for или хотя бы неявно подразумеваемую последовательность исполнения операторов (действия y1, y2, y3).
Для блок-схемы в одном квадратике можно без проблем перечислить действия и это не изменит последовательный характер их работы. Если же у автомата заменить переходы в состояниях s2, s3 на цикл при состоянии s1, пометив дугу этими же действиями, то смысл алгоритма изменится, т.к. введет параллелизм в его работу. Поскольку упомянутые действия должны исполнятся строго последовательно, то это и предопределило вид модели автомата (см. рис.1). Конечный автомат модель, не допускающая двоемыслия.
Отсутствие дискретного времени у блок-схем было их преимуществом. Но сейчас это стало их основным недостатком. Это влияет, как это не покажется кощунственным, и на образ мышления. Последовательными языками оправдывают последовательное мышление программистов, отказывая им в другом в параллельном. Именно этим обосновывают конструкции существующего асинхронного программирования, представляя набор и функциональность операторов того же пакета asyncio. И именно этот подход, как утверждают, позволяет привычные программистам последовательные программы превратить в асинхронные (почти что параллельные).
Но вернемся к теме статьи и ее образам. Мы хотели свой самолет и мы его получили! Полеты или, что точнее, видимые их результаты, несколько отличаются по внешнему виду, но совершенно неотличимы по своей сути. Их можно трактовать так, что флажки подбирались и фиксировались в протоколе в ином порядке, но сами самолеты при этом летели, как и должно, т.е. одновременно и одновременно выбрасывали свои флажки. А уж в каком порядке их фиксировали дело, как говорится, десятое. Главное реализовано и иполнено: последовательность и времена их сбрасывания соответствуют программе полета
Код можно сократить. Так, по-видимому, можно ограничиться кодом только одного класса. Можно скрыть и код событийного цикла. Если же при этом в исходном коде открыть подкапотный код, который сокрыт за операторами asyncio и await, то объем автоматного кода, скорее всего, уже не будет таким уж отпугивающим.
3. О проблемах реализации автоматов в Python
Остановимся подробнее на проблемах, которые породили внешний вид автоматного кода. Последний, что там скрывать, выглядит пока монструозно в сравнении с исходным кодом. Но, заметим, и первый самолет Можайского далеко не был похож на современную сушку, да и первые автомобили отличаются далеко не в лучшую сторону от любого своего современного аналога. Подчеркну, что проблемы предъявленного автоматного кода во многом связаны с моим текущим пониманием языка Python и, возможно, в меньшей степени с возможностями самого языка.
Тем не менее первая проблема связана с языком описания автоматной модели. В С++ она решена средствами языка. В Python я таких возможностей не вижу. К сожалению, как сейчас иногда выражаются, от слова совсем. Поэтому за основу был взят метод реализации автоматов на базе управляющих операторов языка if-elif-else. Кроме того, напомним, в ВКП(а), кроме собственно автоматов, для полноценной реализации параллелизма введена теневая память и автоматные пространства. Без этого возможности автоматного программирования весьма ограничены и во многом неполноценны.
Следующая проблема, которую мы уже упоминали, указатели. В С++ с ними проблем нет. В рамках ВКП(а) в соответствии с парадигмой ООП создан базовый автоматный класс, от которого порождаются прикладные автоматные классы, а их параметрами могут быть не только указатели, но даже их адреса. Все это позволяет просто, компактно и весьма эффективно описать и реализовать любую задачу, включающую множество параллельных взаимодействующих процессов.
Далее представлен код эквивалентных рассматриваемому примеру автоматных классов на С++. Код задержки на листинге 4 эквивалентен строке await asyncio.sleep(1) на листинге 1. В графической форме ей соответствует модель автомата FAwaitSleep на рис. 1. Такой и только такой автомат можно считать асинхронным и он не будет тормозить вычислительный поток. Автомат FSleep на этом же рисунке соответствует обычному оператору sleep(). Он проще, но гарантированно разрушит модель дискретного времени из-за действия y1, вызывающего обычную последовательную задержку. А это уже ни куда не годится.
// Задержка (в дискретном времени)#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; }
Код самолета-паука на С++ демонстрирует листинг 5. Данный код в гораздо большей степени адекватен своей модели, чем блок-схема коду на Python. Особенно, если сравнить таблицу переходов автомата и внешний вид автоматного графа. Это просто разные формы описания одного и того же абстрактного понятия автомата. Здесь же показано, как передается указатель на родительский класс при создании задержки (см. вызов метода FCall в действии y1)
// "Паук". Имитация страниц сайта#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; }
Код, реализующий функции так называемого событийного цикла, отсутствует. В нем просто нет необходимости, т.к. его функции исполняет ядро среды ВКП(а). Оно создает объекты и управляет их параллельным исполнением в дискретном времени.
4. Выводы
Краткость не всегда сестра таланта, а иногда еще и признак косноязычия. Правда, отличить одно от другого сразу сложно. Код на Python часто будет короче кода на С++. Но это характерно для простых случаев. Чем сложнее решение, тем эта разница будет меньше. В конце концов даже сложность решения определяется возможностями модели. Автоматная модель гораздо мощнее блок-схемной.
Автоматы и распараллеливание это в первую очередь весьма эффективные средства решения проблем сложности, борьбы с нею, а не столько средства увеличения скорости работы программы. Поскольку все это автоматная модель, параллелизм сложно реализуемо на Python, то, несмотря на все его фишки, батарейки и много еще чего, меня сложно склонить в его сторону. Я бы больше уделял внимание окружению С++, а не очень оправданному внедрению в него тех же корутин. Модель эта временная и причина ее внедрения во многом вынужденная. А что мы будем делать с этим костылем, когда будет решена проблема выбора параллельной модели?
Поэтому, извините, но мои предпочтения все еще на стороне С++. А если учесть область моих профессиональных интересов промышленные системы так называемого жуткого жесткого реального времени, то выбора как такового у меня пока что нет. Да, какое-то окружение, какой-то сервис можно создать, используя Python. Это удобно, это быстро, есть много прототипов и т.д. и т.п. Но ядро решения, его параллельную модель, логику самих процессов однозначно С++, однозначно автоматы. Здесь автоматы, конечно, главнее и рулят. Но никак не корутины :)
В дополнение Посмотрите видео [2], обратив внимание на реализацию модели ракеты. О ней, начиная примерно с 12-й минуты, и повествует видео. Респект лектору за использование автоматов :) А на сладкое предлагается еще одно решение из [3]. Оно в духе асинхронного программирования и asyncio. Собственно с этого примера все и началось реализация вложенных автоматов в Python. Здесь глубина вложения даже больше, чем в примере, подробно рассмотренном выше. На листинге 6 приведен исходный код и его автоматный аналог на Python. На рис. 2 автоматная модель чаепития, а на листинге 7 эквивалентная реализация на С++ для ВКП(а). Сравнивайте, анализируйте, делайте выводы, критикуйте
import asyncioimport 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 = 4class 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 = 4class 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 = Falseread = PRead()rt = PTeaTime(read)sl = PSleep(5, rt)automaton = []automaton.append(read); automaton[-1].bIfLoop = Truewhile 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
#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
Уже после написания статьи после чтения перевода статьи Йерайна Диаза[4], познакомился с еще одним достаточно интересным и довольно восхищенным взглядом на корутины вообще и asyncio в частности. Несмотря на этот факт и ему подобные, мы все же пойдем иным путем :) Согласен только в одном с Робом Пайком (Rob Pike), что Concurrency Is Not Parallelesm. Конкурентность, можно сказать даже жестче, не имеет вообще ни чего общего с параллелизмом. И примечательно, что Google-переводчик переводит эту фразу как Параллелизм это не параллелизм. Мужчина по имени Google, конечно, не прав. Но кто-то же его в этом убедил? :)
Литература
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 для практикующего python-разработчика. [Электронный ресурс], Режим доступа: www.youtube.com/watch?v=PaY-hiuE5iE, свободный. Яз. рус. (дата обращения 01.08.2020).