Pull to refresh

Comments 26

А как обстоит дело с потокобезопасностью в вашем примере? Например, объект 'current' — я правильно понимаю что вы просто опустили синхронизацию для упрощения?
Никак, все происходит в одном потоке. Прелесть сопрограмм в том что они хорошо реализуют событийно-управляемую многозадачность (вытесняемую многозадачность) в одном потоке. Синхронизации нет, так как нечего синхронизировать, current нужно для отслеживания текущей (выполняющейся в данный момент) сопрограммы.
Вытесняемая многозадачность, события и один поток в одном предложении, как мне кажется, образуют какую-то кашу. Под вытеснением обычно же понимают тот факт, что поток исполнения может переключится не по своему желанию. В этом же случае если сопрограмма написана криво, то она захватит поток исполнения навечно, разве нет?
Безусловно, в скобочках должно быть написано (кооперативная многозадачность). Размышлял когда писал пост, почему оппоненту показалось что в примере задействованы потоки и выдал другое название обычной многопоточной многозадачности.
Вообще говоря это не вытесняющая, а кооперативная многозадачность. Есть event-loop, который шедулит сопрограммы. Ситуации, когда посреди выполнения синхронного кода (без await) будет переключен контекст на другую сопрограмму случиться не может.
Вообще-то постом выше и раньше вашего я признал свою ошибку и даже попытался ее объяснить.
т.е. всегда необходимо запускать луп и на нем всегда выполнение основного кода завершается? в том смысле, что после него нет смысла чего-нибудь выполнять.
как запускать сопрограмму выполняющую периодически какое-либо действие? например, сканирующую каталоги и обновляющую какой-нибудь массив, но при этом другая сопрограмма должна что-то с этим массивом делать?
вообще, какое применение данной функциональности может быть, если все выполняется в одном потоке?:) ну, кроме tcp-клиента или ожидания расчета факториала:)
  1. Да запуск диспетчера событий, это последнее что запускается в основном потоке. Исключение может быть только что-то, что должно выполнится перед завершением программы.
  2. Запускать ее по таймеру и последующим перезапуском, точно так же как в примере.
  3. Сканировать каталоги можно, но придется периодически отдавать выполнение в другие сопрограммы (скажем уходить в ожидание таймаута (sleep), как в примере).
  4. Основной usecase наверно однопоточный сетевой сервис, у которого быстро формируется ответ. Но никто не мешает запускать несколько потоков и обменом между ними сообщениями. В этом случае usecase уже можно сильно расширить.


Хорошая статья и хороший подход с async/await. Python как всегда всё больше радует с каждой версией.
>Использование термина «асинхронный код» может ввести в заблуждение, потому-что «асинхронный код» часто реализуется на функциях обратного вызова, а это немного другая тема.

В том-то и вся прелесть реализации асинхронности Python: вместо лапши из callback’ов, вы пишете обычный «синхронный» код, лишь изредка добавляя «async» и «await» в местах блокировок I/O, и получаете полностью асинхронную программу.
Все верно, хочу лишь заметить, что все предыдущие реализации coroutine в Pythone, будь то реализация на генераторах в asyncio и tornado, etc или реализация на greenlet тоже позволяли писать обычный «синхронный» код без коллбеков, и даже без изредкого добавления «async» и «await».
Если честно, то конструкция await/async не слишком выразительна. Какая-то мешанина из обработчиков и генераторов.
На практике я python3 не использую, потому мои наблюдения могут не иметь смысла.

Попытался сочинить что-то идя по вашим стопам и докам в сети, получилось такое:
import asyncio

async def hello(name, timeout):
    await poke(name, timeout)

class poke:
    def __init__(self, name, timeout):
        self.name = name
        self.timeout = timeout

    def __await__(self):
        yield from asyncio.sleep(self.timeout) # say hello
        if not self.name.startswith("world"):
            yield from asyncio.wait([hello("world.{}".format(self.name), 0.3)])
        yield

async def friends():
    await asyncio.wait([
        hello("friends", 0.5),
        hello("neighbours", 0.3),
    ])

loop = asyncio.get_event_loop()
loop.run_until_complete(friends())


Вроде все понятно. А теперь я решил включить мозги и попробовать без asyncio:
import time

async def hello(name, timeout):
    await poke(name, timeout)

class poke:
    def __init__(self, name, timeout):
        self.name = name
        self.timeout = timeout

    def __await__(self):
        time.sleep(self.timeout) # say hello
        if not self.name.startswith("world"):
            coro = hello("world.{}".format(self.name), 0.3)
            while True:
                try:
                    coro.send(None)
                    yield
                except StopIteration:
                    break
        yield

async def friends():
    coros = [
        hello("friends", 0.5),
        hello("neighbours", 0.3)
    ]
    for coro in coros:
        coro.send(None)
    for coro in coros:
        await coro

poll = friends()
while True:
    try:
        poll.send(None)
    except StopIteration:
        break


Второй вариант выглядит коряво и асимметрично. В чем дело? Так и должно быть?
Почему нельзя вызывать `await` в `__await__` методе? Почему для `friends` я также не могу вызвать `await`, но должен в цикле слать `send`?
Также я заметил, что порядок выполнения разный, хотя все стадии проходит в обоих случаях. Если честно, то не совсем понятно почему так получается.
Не обижайтесь, но вы либо совсем ничего не поняли, либо ваш опыт использования кода на callback, был категорически против загрузки в голову :) материала про coroutine. Видно четко, что вы не поняли преимуществ, которые дает использование сопрограмм и пытались притянуть в примеры концепцию функций обратного вызова. Я сейчас прокомментирую ваш код с asyncio и дам свой вариант.

  • Сопрограмма созданная вызовом hello у вас завершается не успев сделать чего-то существенного, кроме ожидания своего завершения.
  • Зачем вы создаете и запускаете другую сопрограмму в await объекте?
  • Добавление «world.», может я не понял идеи. Если это для ограничение итераций, то подход абсолютно неверный — в самой короутине это можно и нужно сделать.
  • Функции asyncio такие как sleep, wait, etc. уже возвращают awaitable объект. Использовать с ними yield from не надо.
  • def friends() — это просто неоправданное увеличение энтропии :)


import asyncio

async def hello(name, timeout):
    cnt = 0
    while True and cnt < 5:
        await asyncio.sleep(timeout)
        print("Hello, {}".format(name))
        cnt += 1

if __name__ == '__main__':

    tasks = [
        hello("friends", 0.5),
        hello("neighbours", 0.3),
    ]

    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()


В втором варианте, к проблемам первого еще и добавляется event-driven. Вам надо почитать больше про событийное программирование. Предыдущая моя статья немного этого касалась. А как пример, ну я уже приводил пример в статье без asyncio. Единственно могу сделать пример без tornado loop со своим простейшим диспетчером событий. Но это если интересно напишите, отдельным постом сделаю.
Спасибо за ответ.
Не обижайтесь, но вы либо совсем ничего не поняли, либо ваш опыт использования кода на callback, был категорически против загрузки в голову :) материала про coroutine. Видно четко, что вы не поняли преимуществ, которые дает использование сопрограмм и пытались притянуть в примеры концепцию функций обратного вызова.

Я не обижаюсь, а пытаюсь разгрести кашу в голове. С coroutine я знаком — libevent в C, Fiber в ruby — в данных случаях у меня не возникало проблем с пониманием. В вашем примере сложно отследить что за чем следует. Да, если использовать coroutine_start и закрыть глаза на все остальное — ничего сложного. К стати не совсем понятно зачем `current.append(coro)` в `switch_to` — чтоб поддерживать бесконечный цикл в `hello`? Зачем там вообще бесконечный цикл? Если убрать бесконечный цикл, то обьект sleep не будет функционировать правильно(список current всегда будет содержать лишние `hello`) и, если я правильно понимаю, в определенный момент выскочит исключение.

В своем примере я попытался опробовать coroutine generator и вложенные вызовы этих же coroutine.
Сопрограмма созданная вызовом hello у вас завершается не успев сделать чего-то существенного, кроме ожидания своего завершения.

Ну как же — запускает coroutine `poke`. Да, не возвращает результат, но это пример сказать «привет» — считайте keep-alive.
Зачем вы создаете и запускаете другую сопрограмму в await объекте?

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

В данном случае чтоб избежать бесконечной рекурсии. Да, лучше бы вызвал не hello, а что-то другое.
Функции asyncio такие как sleep, wait, etc. уже возвращают awaitable объект. Использовать с ними yield from не надо.

Если я правильно понимаю, то в `__await__` можно использовать либо `yield from` либо `return`. Python3.5 не разрешает использовать `await` в `__await__`
def friends() — это просто неоправданное увеличение энтропии :)

Не совсем понятно данное утверждение.

Попытался соорудить более понятный пример, в котором я приглашаю друзей на «вечеринку»:
import asyncio

async def say(name, what, timeout):
    return await poke('{} {}'.format(what, name), timeout)

async def ping(timeout):
    await asyncio.sleep(timeout)
    return 'OK'

async def handshake(timeout):
    await asyncio.sleep(timeout)
    return 'OK'

class poke:
    def __init__(self, name, timeout):
        self.name = name
        self.timeout = timeout

    def __await__(self):
        res = yield from asyncio.wait_for(ping(0.1), None)
        assert res == 'OK'
        res = yield from asyncio.wait_for(handshake(0.1), None)
        assert res == 'OK'
        return 'OK'

async def invite_friends():
    res, _ = await asyncio.wait([
        say("friends", 'hello', 0.5),
        say("neighbours", 'hello', 0.3),
    ])
    assert all(([x.result() == 'OK' for x in res]))

loop = asyncio.get_event_loop()
loop.run_until_complete(invite_friends())


Возможно я не понимаю основное идеи Future-like объектов? В моем случае можно было бы и обойтись `async coroutine`, конечно. Возможно Вы сможете дать наглядный пример использования подобных объектов?

Также все еще интересно увидеть комментарии ко второй части моего оригинального поста.
Посмотрел на ваш следующий пример и еще раз вам пишу, вы используете сопрограммы в подходе как использовали бы функции обратного вызова. Зачем? Сформулируйте мне какую задачу реализовываете в примере. Я дам свой пример, возможно это поможет разобраться. А возможно задача у вас вообще не укладывающееся в async/awaite, и пытаясь ее все же решить через coroutine вы приходите к таким странным вещам как сопрограммы делающие только то, что дожидающиеся своего завершения.
`current.append(coro)` — `current[0]` Будет содержать текущую выполняющуюся сопрограмму. В том примере это не совсем нужно, но если бы был запуск другой короутины из исполняющейся без этого было бы не обойтись.
`switch_to` — не поддерживает цикл, а возвращает управление в короутину.

Во втором примере самое трагическое это строка `time.sleep(self.timeout) # say hello` эта строка останавливает выполнение Python кода полностью. Соответственно ни о какой кооперативной многозадачности речи уже не идет.
Я думаю смысла разбираться с тем нет, пока не разберемся с назначением сопрограммы. В примере с asyncio хотя бы диспетчер событий и awaitable объекты уже готовы.
Посмотрел на ваш следующий пример и еще раз вам пишу, вы используете сопрограммы в подходе как использовали бы функции обратного вызова. Зачем? Сформулируйте мне какую задачу реализовываете в примере. Я дам свой пример, возможно это поможет разобраться. А возможно задача у вас вообще не укладывающееся в async/awaite, и пытаясь ее все же решить через coroutine вы приходите к таким странным вещам как сопрограммы делающие только то, что дожидающиеся своего завершения.

Замените asyncio.sleep на что-то вроде loop.sock_sendall+loop.sock_recv. Так лучше понятна задача? Использовал asyncio.sleep для эмуляции задержек в качестве примера.
Задача такова — есть список друзей, нужно всех пригласить на праздник. Чтоб пригласить на праздник сначала нужно дозвониться(ping), потом уговорить прийти(handshake). Представим, что я могу звонить всем одновременно, и пока один тупит — могу говорить с другим. Пока говорю с одним — все ждут. Собственно под этим я и понимаю coroutines в single-threaded event loop.

Почему я не могу использовать coroutines в данном примере? Как бы вы реализовали эту задачу?

Возможно необходимо было использовать `s/say/invite/`чтоб было более очевидно, согласен.
Ради наглядности переписал бы как-то так:
import asyncio
import logging
from random import random

logging.getLogger().setLevel(logging.DEBUG)

class dialog:
    def __init__(self, name, latency):
        self.name = name
        self.latency = latency

    async def call(self):
        logging.debug('calling {}'.format(self.name))
        await asyncio.sleep(self.latency/2+random())
        return 'OK'

    async def convince(self):
        logging.debug('convincing {}'.format(self.name))
        await asyncio.sleep(self.latency/2+random())
        return 'OK'

    def __await__(self):
        res = yield from asyncio.wait_for(self.call(), None)
        assert res == 'OK'
        res = yield from asyncio.wait_for(self.convince(), None)
        assert res == 'OK'
        logging.debug('invited {}'.format(self.name))
        return 'OK'

async def invite(name, latency):
    return await dialog(name, latency)

async def invite_friends():
    friends = [
        # (name, latency)
        ('mark', 0.5),
        ('bob', 0.3),
    ]
    coros = [invite(name, latency) for name, latency in friends]
    res, _ = await asyncio.wait(coros)
    assert all(([x.result() == 'OK' for x in res]))

loop = asyncio.get_event_loop()
loop.run_until_complete(invite_friends())

Почему я не могу использовать coroutines в данном примере?
Я не говорил, что в этой задачи нельзя использовать coroutine, не внимательно прочитали мой ответ?
Как бы вы реализовали эту задачу?
Задачу вы описали, я ее понял, и мой пример будет ниже. Теперь мне и стало понятно что у вас не так. Зачем делать логику, тем более прикладную в awaitable объекте? Он для этого не предназначен. Делайте всю логику в сопрограммах. Пример ниже, возможно многословный, но просто хотелось красивый лог:

import random
import logging
import asyncio

async def call_to(name):
    cnt = 0
    max_ring = 7
    result = False
    logging.debug("Calling {} ...".format(name))
    attempts = random.randrange(0, 9, 1) + 1

    while cnt < attempts:
        await asyncio.sleep(1.0)
        logging.debug("({}): beep".format(name))
        cnt += 1
        if cnt == max_ring:
            logging.debug("({}): not picked up".format(name))
            break
    else:
        result = True
    return result


async def sell_on(name):
    cnt = 0
    max_offer = 3
    logging.debug("Responding {} ...".format(name))
    while True:
        cnt += 1
        await asyncio.sleep(1.0)
        answer = random.randrange(0, 3, 1)
        if answer == 2:
            logging.debug("({}): Yes, I will come".format(name))
            return True
        elif  answer == 1:
            logging.debug("({}): No, I will not come".format(name))
            return False
        else:
            if cnt == max_offer:
                logging.debug("({}): No, I will not come".format(name))
                return False
            else:
                logging.debug("({}): Maybe, I don't know".format(name))


async def invite(name, result):
    answered = await call_to(name)
    if answered:
        agreed = await sell_on(name)
        result.append((name, agreed))
    else:
        result.append((name, answered))


if __name__ == '__main__':

    logging.basicConfig(level=logging.DEBUG)

    result = list()
    frends = ['Саша', 'Паша', 'Катя', 'Маша', 'Дуся', 'Маруся', 'Ваня']
    tasks = [invite(name, result) for name in frends]

    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

    print("\n----------------------------------------")
    for name, agreed in result:
        print("{}\t{}".format(name, "придет" if agreed else "не придет"))

    loop.close()


Можно sell_on переписать более интересно:

async def sell_on(name):
    cnt = 0
    max_offer = 3
    logging.debug("Responding {} ...".format(name))
    while True:
        cnt += 1
        answer = random.randrange(0, 9, 1) + 1
        await asyncio.sleep(answer)
        if answer % 2:
            logging.debug("({}): Yes, I will come".format(name))
            return True
        else:
            logging.debug("({}): No, I will not come".format(name))
            return False
Спасибо за развернутый ответ.

Грубо говоря тоже самое, что и в моем примере, только без future-like object.

Я так понял наше недопонимание возникло из-за этого самого furure-like object. Как я упоминал ранее — хотел на самом деле пощупать что оно такое. Исходя из ваших ответов, я не представляю что это за фрукт но все-таки хотелось бы понять суть awaitable объекта и почему по вашему мнению он здесь не клеится.
Мне кажется, ну собственно и в документации awaitable объект упоминается и применяется только качестве, специализированного объекта переключающего управление с/на сопрограмму с возвратом или нет результата выполнения асинхронного действия. Во всяком случае примеров нет, которые бы более полно или както по другому раскрыли его назначение.
Зачем нагружать его еще какой-то логикой, если это удобнее и логичнее сделать в суб. сопрограмме вызываемой из текущей сопрограммы с помощью ключевого слова await. Здесь логика похожа на вложенные генераторы, вызываемые с помощью yield from, вернее не похоже, а по внутренней реализации тоже самое.
В том то и дело, что примеров нет.

Осталось только услышать ответ эксперта по поводу использования голого async/await без сторонних библиотек типа tornado(ok, asyncio встроен в python, но он же не реализует все на свете). Собственно меня смущает как coroutine запускается(см. второй кусок кода из моего первого комментария). С time.sleep понятно(хотя не логично — могли бы допилить), но вызов в цикле который прерывается по исключению StopIteration — как по мне либо выглядит убого.

На вопрос зачем — а вдруг я не хочу тянуть весь asyncio просто потому, что хочу баловаться python на устройстве с ограниченными ресурсами.
Осталось только услышать ответ эксперта по поводу использования голого async/await без сторонних библиотек

Окей) Что нам дает asyncio, из того что мы использовали в своих примерах? Оно нам дает event loop (диспетчер событий) и функцию sleep, которая возвращает awaitable. Этот awaitable делает следующее, переключает управление на event loop предварительно каким-то образом наладив event loop на возврат управления в текущую сопрограмму через заданный промежуток времени.

В примере, в статье, я использовал event loop tornado, а awaitable объект написал сам. Можем его разобрать если не вполне понятно, что там происходит.

Как писать диспетчер, я приводил пример в предыдущей статье, но в принципе тема event loop не имеет прямого отношения к coroutine ибо с таким же успехом используется в асинхронных программах построенных на функциях обратного вызова.
Насчет «не хочу тянуть весь asyncio», да из него слепили монстра на все случаи жизни. Хотя в большинстве своем достаточно event loop и трех awaitable объектов типо sleep, wait_io, wait_signal. Я как раз собираюсь исправить этот недостаток asyncio :) если не потеряю интерес и мотивацию.
Надеюсь не потеряете. Тема для python интересная, а по сути кроме asyncio никакой «легковесной» альтернативы или примеров реализации в сети нет =/
Sign up to leave a comment.

Articles