我如何在异步中使用请求?

Posted

技术标签:

【中文标题】我如何在异步中使用请求?【英文标题】:How could I use requests in asyncio? 【发布时间】:2014-04-07 01:51:03 【问题描述】:

我想在asyncio做并行http请求任务,但是我发现python-requests会阻塞asyncio的事件循环。我找到了aiohttp,但它无法使用http代理提供http请求的服务。

所以我想知道有没有办法在asyncio的帮助下进行异步http请求。

【问题讨论】:

如果您只是发送请求,您可以使用subprocess 来并行您的代码。 这个方法好像不太优雅…… 现在有一个异步请求端口。 github.com/rdbhost/yieldfromRequests 这个问题对于间接依赖于requests(如google-auth)并且不能简单地重写以使用aiohttp的情况也很有用。 【参考方案1】:

Requests 目前不支持asyncio,并且没有提供此类支持的计划。您可能可以实现一个自定义的“传输适配器”(如 here 所讨论的),它知道如何使用 asyncio

如果我发现自己有一些时间,我可能会真正研究一下,但我不能保证任何事情。

【讨论】:

链接指向 404。【参考方案2】:

要将请求(或任何其他阻塞库)与 asyncio 一起使用,您可以使用 BaseEventLoop.run_in_executor 在另一个线程中运行一个函数并从中获取结果。例如:

import asyncio
import requests

@asyncio.coroutine
def main():
    loop = asyncio.get_event_loop()
    future1 = loop.run_in_executor(None, requests.get, 'http://www.google.com')
    future2 = loop.run_in_executor(None, requests.get, 'http://www.google.co.uk')
    response1 = yield from future1
    response2 = yield from future2
    print(response1.text)
    print(response2.text)

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

这将同时获得两个响应。

在 python 3.5 中,您可以使用新的 await/async 语法:

import asyncio
import requests

async def main():
    loop = asyncio.get_event_loop()
    future1 = loop.run_in_executor(None, requests.get, 'http://www.google.com')
    future2 = loop.run_in_executor(None, requests.get, 'http://www.google.co.uk')
    response1 = await future1
    response2 = await future2
    print(response1.text)
    print(response2.text)

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

请参阅PEP0492 了解更多信息。

【讨论】:

你能解释一下它是如何工作的吗?我不明白这怎么不阻止。 @christian 但如果它在另一个线程中同时运行,那不是破坏了异步的意义吗? @christian 是的,关于它触发呼叫并恢复执行的部分是有道理的。但如果我理解正确,requests.get 将在另一个线程中执行。我相信 asyncio 的一大优点是保持单线程的想法:不必处理共享内存、锁定等。我认为我的困惑在于您的示例同时使用 asyncio 和 concurrent.futures 模块. @scoarescoare 这就是“如果你做对了”部分的用武之地——你在执行器中运行的方法应该是独立的((大部分)就像上面例子中的 requests.get 一样)。这样您就不必处理共享内存、锁定等问题,而且由于 asyncio,您的程序的复杂部分仍然是单线程的。【参考方案3】:

aiohttp 已经可以与 HTTP 代理一起使用:

import asyncio
import aiohttp


@asyncio.coroutine
def do_request():
    proxy_url = 'http://localhost:8118'  # your proxy address
    response = yield from aiohttp.request(
        'GET', 'http://google.com',
        proxy=proxy_url,
    )
    return response

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

【讨论】:

连接器在这里做什么? 通过代理服务器提供连接 这是一个比在单独线程中使用请求更好的解决方案。由于它是真正异步的,因此开销和内存使用率更低。 for python >=3.5 将@asyncio.coroutine 替换为“async”,将“yield from”替换为“await”【参考方案4】:

Pimin Konstantin Kefaloukos 的文章中有一个很好的异步/等待循环和线程案例 Easy parallel HTTP requests with Python and asyncio:

为了最小化总完成时间,我们可以增加线程池的大小以匹配我们必须发出的请求数。幸运的是,这很容易做到,我们将在接下来看到。下面的代码清单是一个示例,说明如何使用包含 20 个工作线程的线程池发出 20 个异步 HTTP 请求:

# Example 3: asynchronous requests with larger thread pool
import asyncio
import concurrent.futures
import requests

async def main():

    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:

        loop = asyncio.get_event_loop()
        futures = [
            loop.run_in_executor(
                executor, 
                requests.get, 
                'http://example.org/'
            )
            for i in range(20)
        ]
        for response in await asyncio.gather(*futures):
            pass


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

【讨论】:

这个问题是,如果我需要运行 10000 个包含 20 个执行程序的请求,我必须等待所有 20 个执行程序完成才能从下一个 20 个执行程序开始,对吗?我不能为for i in range(10000) 做,因为一个请求可能会失败或超时,对吧? 你能解释一下为什么你需要 asyncio,而你可以使用 ThreadPoolExecutor 来做同样的事情吗? @lya Rusin 基于什么,我们设置 max_workers 的数量?跟CPU和线程的数量有关系吗? @AsafPinhassi 如果你的脚本/程序/服务的其余部分是异步的,你会想“一直”使用它。你可能最好使用 aiohttp(或其他一些支持 asyncio 的库) @alt-f4 实际上你有多少 CPU 并不重要。将这项工作委托给线程(以及 asyncio 的全部内容)的目的是针对 IO 绑定操作。线程将简单地通过空闲(“等待”)从套接字检索响应。 asyncio 能够在根本没有线程(嗯,只有一个)的情况下实际处理许多并发(不是并行!)请求。但是requests 不支持异步,所以需要创建线程来获得并发。【参考方案5】:

上面的答案仍然使用旧的 Python 3.4 风格的协程。如果你有 Python 3.5+,这是你会写的。

aiohttp 现在支持http代理

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
            'http://python.org',
            'https://google.com',
            'http://yifei.me'
        ]
    tasks = []
    async with aiohttp.ClientSession() as session:
        for url in urls:
            tasks.append(fetch(session, url))
        htmls = await asyncio.gather(*tasks)
        for html in htmls:
            print(html[:100])

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

【讨论】:

你能详细说明更多的网址吗?当问题是关于并行 http 请求时,只有一个 url 是没有意义的。 传奇。谢谢!效果很好 @ospider 如何修改此代码以使用 100 个请求并行发送 10k URL?这个想法是同时使用所有 100 个插槽,而不是等待交付 100 个才能开始下一个 100 个。 @AntoanMilkov 那是另一个问题,无法在评论区回答。 @ospider 你说得对,问题来了:***.com/questions/56523043/…【参考方案6】:

免责声明:Following code creates different threads for each function.

这可能对某些情况有用,因为它更易于使用。但是要知道它不是异步的,而是使用多个线程产生异步的错觉,尽管装饰器建议这样做。

要使任何函数非阻塞,只需复制装饰器并使用回调函数作为参数装饰任何函数。回调函数会接收函数返回的数据。

import asyncio
import requests


def run_async(callback):
    def inner(func):
        def wrapper(*args, **kwargs):
            def __exec():
                out = func(*args, **kwargs)
                callback(out)
                return out

            return asyncio.get_event_loop().run_in_executor(None, __exec)

        return wrapper

    return inner


def _callback(*args):
    print(args)


# Must provide a callback function, callback func will be executed after the func completes execution !!
@run_async(_callback)
def get(url):
    return requests.get(url)


get("https://google.com")
print("Non blocking code ran !!")

【讨论】:

【参考方案7】:

考虑到 aiohttp 是功能齐全的 Web 框架,我建议使用更轻量级的东西,例如支持异步请求的 httpx (https://www.python-httpx.org/)。它具有与请求几乎相同的 api:

>>> async with httpx.AsyncClient() as client:
...     r = await client.get('https://www.example.com/')
...
>>> r
<Response [200 OK]>

【讨论】:

有一篇很好的文章涵盖了这个主题blog.jonlu.ca/posts/async-python-http

以上是关于我如何在异步中使用请求?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 saga 异步请求后设置状态

如何保证异步请求中的正确顺序

如何在 Rails 中执行异步请求?

Swift Siesta - 如何将异步代码包含到请求链中?

如何在 React 应用程序中使用 JEST 测试向 api 发出 axios 请求的异步函数

如何在 Django 中异步发送 API 请求过程?