Параллелизуйте веб-задачи с помощью asyncio в Python

Я пытаюсь разобраться с asyncio и aiohttp, и впервые за многие годы программирование заставляет меня чувствовать себя совершенно глупым и неспособным. Что довольно красиво, в духе странного дзен. Но, увы, есть над чем поработать.

У меня есть существующий класс, который может делать множество удивительных вещей в Интернете, таких как регистрация на веб-сайте, получение данных, работа. И теперь мне нужно около 100 или 1000 этих маленьких рабочих пчел, чтобы зарегистрироваться. Код выглядит примерно так:

class Worker(object):
    def signup(self, ...):
        ...
        data = self.make_request(url, data)
        self.user_id = data.get("user_id")
        return self

    def make_request(self, url, data):
        response = requests.post(url, data=data)
        return response.json()

workers = [Worker().signup() for n in range(100)]

Как видите, мы используем модуль запросов для выполнения POST-запроса. Однако это блокировка, поэтому нам придется подождать, пока работник N завершит регистрацию, прежде чем мы начнем регистрировать работника N+1. К счастью, первоначальный автор класса Worker (это звучит очаровательно по-марксистски) в своей бесконечной мудрости заворачивал каждый HTTP-вызов в метод self.make_request, поэтому сделать весь Worker неблокирующим можно просто путем замены библиотеки запросов на неблокирующую. блокирует один ааааааааааааааа твой дядя боб, да? Вот как далеко я продвинулся:

class AyncWorker(Worker):
    @asyncio.coroutine
    def make_request(self, url, data):
        response = yield from aiohttp.request('post', url, data=data)
        return (yield from response.json())

coroutines = [Worker().signup() for n in range(100)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coroutines))
loop.close()

Но это поднимет AttributeError: 'generator' object has no attribute 'get' в методе signup, где я делаю self.user_id = data.get("user_id"). Кроме того, у меня до сих пор нет рабочих слов в аккуратном словаре. Я знаю, что я, скорее всего, совершенно неправильно понимаю, как работает asyncio, но я уже провел день, читая различные документы, умопомрачительные учебники Дэвида Бизли и массу игрушечных примеров, которые достаточно просты, чтобы я понял их и слишком просто применить в этой ситуации. Как мне структурировать моего воркера и мой асинхронный цикл, чтобы зарегистрировать 100 воркеров параллельно и в конечном итоге получить список всех воркеров после их регистрации?


person Manuel Ebert    schedule 28.10.2014    source источник
comment
После того, как вы прикоснетесь к сопрограммам asyncio, вы ДОЛЖНЫ работать с ними как сопрограммы. Это означает, что ваш метод signup должен быть сопрограммой, которая делает что-то вроде data = yield from self.make_request(url, data). Все, что вы возвращаете в сопрограмме asyncio, должно использоваться только с yield from.   -  person x3al    schedule 28.10.2014


Ответы (1)


Как только вы используете yield (или yield from) в функции, эта функция становится сопрограммой. Это означает, что вы не можете получить результат, просто вызвав его: вы получите generator object. Вы должны по крайней мере сделать это:

@asyncio.coroutine
def some_coroutine(*args):
    #...
    #...
    yield from tasty.asyncio.function()
    return result

def coroutine_user():
    # data = some_coroutine() will give you a generator object instead of result
    data = yield from some_coroutine()
    return data # data here is a plain result: you can call your .get or whatever

Угадайте, что происходит, когда вы звоните coroutine_user():

>>> coroutine_user()
<generator object coroutine_user at 0x7fe13b8a47e0>

Отсутствие async.coroutine декоратора совсем не помогает: сопрограммы заразительны! Чтобы получить результат в функции, вы должны использовать yield from. Это превращает вашу функцию в еще одну сопрограмму!

Хотя не всегда все так плохо (обычно вы можете вручную перебрать объект генератора, не полагаясь на yield from), asyncio специально не позволит вам это сделать: он ломает некоторые внутренние компоненты (вы можете сделать это только из Future или asyncio.coroutine). Так что просто используйте concurrent.futures или что-то подобное, если вы не собираетесь превращать весь свой код в сопрограммы. В качестве альтернативы, изолируйте всех пользователей aiohttp.request от обычных методов и работайте как с асинхронными работниками на основе сопрограмм, так и с синхронным простым старым кодом. Погружение в asyncio и фактический рефакторинг всего вашего кода, очевидно, тоже вариант: вам в основном нужно ставить yield from перед каждым вызовом любого зараженного с помощью asyncio метода.

person x3al    schedule 28.10.2014
comment
Просто для уточнения: вы должны использовать yield from с asyncio сопрограммами, потому что это единственный способ вернуть управление циклу обработки событий и позволить сопрограмме фактически работать. Если бы вам было позволено просто перебирать asyncio.Future, вы бы никогда не вернули контроль над циклом событий, сопрограмма никогда не запустилась бы, и в конечном итоге вы попытались бы получить доступ к результату Future до того, как он станет доступным. concurrent.futures.Future получает параллелизм от нескольких потоков или процессов, а не от неблокирующего ввода-вывода и цикла событий, поэтому у него нет этого ограничения. - person dano; 28.10.2014