Pull to refresh

AsyncIO Micropython: методы синхронизации в асинхронном программировании

Reading time 6 min
Views 4.4K
В последовательном программировании я постоянно сталкиваюсь с очевидным желанием не останавливать работу программы в момент, когда целью отдельных задач(процессов) является периодические действия — например, опрос значений датчиков, или передача данных по расписанию на сервер, или ввод/вывод большого объема данных. Самое простое, конечно, дождаться завершения периодического события и затем, не спеша, продолжить выполнять другие задачи.

    while True:
        do_ext_proc_before()
        do_internal_proc()
        sleep(5)
        do_ext_proc_after()

Можно отказаться от 'sleep()' и включить проверку некоторых условий в цикле, что позволит не задерживать основной цикл хотя бы до наступления периодического события:

    start = time()
    set_timer(start,wait=5)       # установить расписание
    set_timeout(start,wait_to=7)  # установить таймаут
    set_irq(alarm)                # установить флаг прерывания

    while True:
        curTime = time()
        do_ext_proc_before()

        if timer(curTime) or timeout(curTime) or alarm:
        # if all events crazy start simultaneously - reset all
            start = time()
            set_timer(start,wait=5)       # установить таймаут
            set_timeout(start,wait_to=7)  # установить таймаут
            set_irq(alarm)                # установить флаг прерывания

            do_internal_proc()
        
        do_ext_proc_after()

В асинхронном программировании каждая задача становится самостоятельным процессом и исполняется, в зависимости от конкретной реализации, параллельно или псевдо-параллельно, используя внутреннее понимание естественных или искусственно установленных условий ожидания или использования ограниченного ресурса, например, диска или канала связи.

    setTask(do_ext_proc_before())
    setTask(do_internal_proc(),timer=5,timeout=7,alarm_handler=alarm)
    setTask(do_ext_proc_after())

    runTasks()

Теперь возникает задача, которая не существует в последовательном программировании — что делать, если необходимо синхронизировать некоторые процессы при их асинхронном
выполнении? Например, получив данные от датчиков инициировать процесс пересылки данных на сервер или отреагировать на аварийную ситуацию. При этом в асинхронном программировании организация асинхронного ввода/вывода органично решается в стандарте языка, а иные ситуации разрешаются в библиотеках.

Я изучал этот вопрос пользуясь расширенной библиотекой asyncio Micropython опубликованной
Peter Hinch(https://github.com/peterhinch/micropython-async/blob/master/TUTORIAL.md)
Простейшее решение — сигнализировать о событии заинтересованным процессам. Для этого служит класс Event(), который содержит несколько модулей

    Event.Set( timeout = None,   data = None )    - установить наступление события (Event = True), например, завершение получения данных от датчиков,
    Event.IsSet()    - проверить, наступило ли событие, возвращает True, если установлено и False если нет
    Event.Wait()     - ждать наступление события, возвращает причину его завершения - Done,Timeout,Cancel
    Event.Data()     - получить данные, связанные с этим событием
    Event.Clear()    - событие завершено  (Event = False). 

Завершение фиксируется, как правило, процессом, ожидающим наступления события, например, процессом отображения на экран или процессом сохранения данных на диск, либо наступлением таймаута, тогда нет необходимости обновлять или сохранять данные, так как они не обновлены по какой-либо причине, либо по причине его прерывания по наступлению иного важного события, например, переходом в спящий режим или перезагрузкой, что может потребовать освобождения всех ожидающих процессов путем сброса соответствующих событий.

Следует учитывать то, что Event.Clear() целесообразно делать только одним процессом, если это не противоречит заданному алгоритму. Иначе, если несколько процессов ожидают появления события Event.Set(), предполагается, что Event.Clear() должен совершить кто-то из заинтересованных процессов, лишь убедившись в том, что все заинтересованные процессы отреагировали на возникшее событие. Что усложняет логику принятия решения при использовании Event-Class при ожидании события несколькими процессами. Эта ситуация решается путем установления определенного количества Clear() возникшего события.

    Barrier.Set( quantity = 1,  timeout = None,  data = None )	  - quantity = 1 адентично Event.Set()
    Barrier.IsSet()  - проверить, наступило ли событие, возвращает True, если установлено и False если нет
    Barrier.Wait()   - ждать наступление события, возвращает причину его завершения - Done,Timeout,Cancel
    Barrier.Data()   - получить данные, связанные с этим событием
    Barrier.qty        - количество еще не завершенных процессов
    Barrier.Clear()   - сбросить событие (Event = False), исполняется каждым ожидающим события процессом уменьшая счетчик Barrier.quantity на единицу, пока он не обнулится, только тогда событие сбросится

При этом не ведется учет — какой конкретно процесс уже отреагировал, а какой еще нет, что может породить проблему повторного реагирования на событие, если это существенно для заданного алгоритма. Если же вместо Barrier.quantity передавать список имен заинтересованных процессов, подобного конфликта можно избежать. Так же, в случае возникновения таймаута или прерывания события можно определить — какие конкретно ожидающие процессы еще не отработали. Все вышесказанное относится к ситуации, когда один или более процессов ожидают наступление некоего события, или ситуации 'один ко многим'. Это возникает в случае, когда процесс или процессы do_ext_proc_after() при последовательном программировании были бы исполнены только после завершения do_internal_proc(). Для удобства дальнейшего понимания расширим существующие Event-Class и Barrier-Class в новый EEvent-Class и сделаем его или объекты, порожденные им — глобальным. Здесь 'creators' — имя или список имен процессов, инициирующих наступление события или разблокировки ресурса, 'folowers' — имя или список имен процессов, ожидающих наступление события или разблокировки ресурса

    EEvent.Set (creators, folowers, timeout = None, data = None ) - возвращает True, если удалось установить событие или заблокировать ресурс
    EEvent.IsSet( procName ) - procName - имя или ID завершившегося процесса
    EEvent.Wait( procName )
    EEvent.Clear( procName )         
    EEvent.Folowers()             - возврашает список незавершенных процессов, еще находящихся в очереди к исполнению. Barrier.qty = len(EEvent.List())
    EEvent.Creators()              - возврашает список процессов, уже инициировавших наступление события

Используя модули EEvent-Class можно так описать решение ранее обсуждаемой задачи

    def do_internal_proc():
        ...
        EEvent.Set ('p_Creator',('p_Folwer1','p_Folwer2'))    # exec 'p_Folwer1','p_Folwer2' after event is come in 'p_Creator'
        ...
        
    def do_ext_proc_after1()
        ...
        EEvent.Wait('p_Creator')
        ...
        EEvent.Clear('p_Folwer1')

    def do_ext_proc_after1()
        ...
        EEvent.Wait('p_Creator')
        ...
        EEvent.Clear('p_Folwer2')

Рассмотрим обратную ситуацию — когда один процесс ожидает завершения нескольких событий, или ситуации 'многие к одному'. Иными словами, что, если выполнение do_internal_proc() может быть только после исполнения do_ext_proc_before() В предельном случае, когда один процесс ожидает завершения/наступления одного события, задача может решаться применением Event-class. Когда же ожидается завершение нескольких событий, например, только после отображения полученных данных и пересылки их на сервер, сохранить их на диск, необходимо, чтобы каждый исполненный процесс установил свое участие в ожидаемом событии и пока все процессы не завершатся, ожидать.

    def do_ext_proc_before1()
        ...
        EEvent.Set('p_Creator1','p_Folwer')
        ...

    def do_ext_proc_before2()
        ...
        EEvent.Set('p_Creator2','p_Folwer')
        ...

    def do_internal_proc():
        ...
        EEvent.Wait(('p_Creator1','p_Creator2'))
        ...
        EEvent.Clear('p_Folwer')
 

Еще один важный аспект асинхронного программирования — совместный доступ к ограниченному ресурсу. Например, обновление данных должно осуществляться только одним процессом, остальные процессы, претендующие на аналогичное действие, должны вставать в очередь или ожидать, пока данные не будут обновлены. В то же время, возможно, что чтение данных для отображения или пересылки, не будет критичным. Поэтому необходимо знать перечень конкурирующих процессов при организации соответствующих событий.

В стандарте асинхронного программирования данная задача решается Lock-Class модулями. В общем случае задачу можно также решить аналогично ситуации ‘один ко многим'

    def do_internal_proc():                                   # lock activity all 'followers' in list
        ...
        EEvent.Set ('p_Creator',('p_Folwer1','p_Folwer2'))    # exec 'p_Folwer1','p_Folwer2' after event is come in 'p_Creator'
        ...
        
    def do_ext_proc_after1()
        ...
        EEvent.Wait('p_Creator')                              # waiting for recourse releale
        if ( EEvent.Set ('p_Folwer1','p_Folwer2')):           # lock resource 'p_Folower1' now is 'p_Creator'
            ...
        else:
            EEvent.Wait('p_Folower2')                         # continue waiting for recourse releale
            ...
        EEvent.Clear('p_Folwer1')                              # releafe recourse

    def do_ext_proc_after1()
        ...
        EEvent.Wait('p_Creator')
        if ( EEvent.Set ('p_Folwer2','p_Folwer1')):           # lock resource 'p_Folower2' now is 'p_Creator'
            ...
        else:
            EEvent.Wait('p_Folower1')                         # continue waiting for recourse releale
            ...
        EEvent.Clear('p_Folwer2')                              # releafe recourse

Кроме рассмотренных вариантов существуют решения, ограничивающие пропускную способность, организующие очереди и управляемую диспетчеризацию процессов, но в моей деятельности пока в этом не появилось необходимости и, как следствие, потребности в достаточном для себя понимании, хотя я не исключаю, что существуют и более изящные или экономные решения.

В заключение хочу сказать, что последовательный и асинхронный подходы имеют равное право на существование и успешно реализуют заданные алгоритмы. Поэтому применение того или иного подхода определяется приоритетами создателя — что для него является более существенным при реализации заданных алгоритмов — прозрачность и читабельность, скорость или объем полученного в результате кода.
Tags:
Hubs:
+9
Comments 1
Comments Comments 1

Articles