python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python asyncio文件描述符最大数量限制

Python解决asyncio文件描述符最大数量限制的问题

作者:IT.BOB

这篇文章主要介绍了Python解决asyncio文件描述符最大数量限制的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

问题复现

Windows 平台下,Python 版本 3.5,使用异步框架 asyncio,有时候会出现 ValueError: too many file descriptors in select() 的报错信息,我们就来聊一下为什么会出现这种问题,以及问题的一些解决方法。

写一个小 dome 复现这个问题(环境:Windows 64 位、Python 3.7):

import aiohttp
import asyncio


num = 0


async def main(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            global num
            num += 1
            print('%s ——> %s' % (str(num), response.status))


def tasks():
    url = 'https://www.baidu.com/s?ie=UTF-8&wd=%s'
    task = [main(url % i) for i in range(10000)]
    return task


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

在打印 500 次左右后就会出现以下报错:

问题分析

好像这个报错和 select 有关,那什么是 select 呢?要怎么解决呢?别急,我们首先来了解一下 asyncio 中的事件循环,即 EventLoop。

事件循环 EventLoop

事件循环是 asyncio 的核心,异步任务的运行、任务完成之后的回调、网络 I/O 操作、子进程的运行,都是通过事件循环完成的,通俗来讲,事件循环所做的就是等待事件发生,然后再将每个事件与我们已明确与所述事件类型匹配的函数进行匹配。

下图很好的展示了协程、事件循环之间的相互作用:

在 asyncio 中,主要提供了两种不同事件循环的实现方法:

那么这两种方法有什么区别呢?在 asyncio 中什么时候用什么方法呢?

我们不妨看一下 asyncio 的源码,在 Python 3.7 中,无论在 Windows 还是 Linux 中都可以看到其默认的设置是 SelectorEventLoop:

我们也可以分别在 Windows 平台和 Linux 平台打印一下 EventLoop 对象(Python 3.7),可以看到默认都是 SelectorEventLoop:

import asyncio

loop = asyncio.get_event_loop()
print(loop)

事实上,在 Python 3.7 以及之前的版本中, 所有平台默认使用的都是 SelectorEventLoop,在 Python 3.8 以及以后的版本中,Unix 平台默认使用的是 SelectorEventLoop,Windows 平台默认使用的是 ProactorEventLoop,这个差异可以在官方文档中看到。

说了这么多,这和 ValueError: too many file descriptors in select() 的报错问题有什么关系呢?select 到底是什么东西呢?

I/O 多路复用

要了解 select,我们还要了解一下什么是 I/O 多路复用(I/O multiplexing),服务器端编程经常需要构造高性能的 I/O 模型,常见的 I/O 模型有同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用等;当需要同时处理多个客户端接入请求时,可以利用多线程或者 I/O 多路复用技术进行处理,I/O 多路复用技术就是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。

select,poll,epoll 等都是 I/O 多路复用的一种机制,其中后两个在 Linux 中可用,Windows 仅支持 select,I/O 多路复用通过这种机制,可以监视多个描述符,一旦某个描述符就绪,一般是读就绪或者写就绪,就是在这个文件描述符进行读写操作之前,能够通知程序进行相应的读写操作。

select 的缺点

I/O 多路复用这个概念被提出来以后, select 是第一个实现这个概念的,select 被实现以后,很快就暴露出了很多问题,其中一个缺点就是 select 在 Windows 中限制了文件描述符数量为 512 个,在 Linux 中限制为 1024 个,那么在前面的 dome 中,使用的是 Python 3.5,这个版本的 asyncio 默认使用了 SelectorEventLoop,底层调用的是 select,受 select 缺点的影响,并发量过高,就出现了 ValueError: too many file descriptors in select() 的报错信息。

解决方法

1.更换事件循环选择器

如果你使用的是 Python 3.7 及以下的版本,那么在 Windows 平台,可以使用 ProactorEventLoop。在 Linux 平台可以使用 PollSelector。

注意:如果你使用了 ProactorEventLoop,那么你将无法使用代理!这是 asyncio 的 bug,早在 2020 年 1 月就有人提过 issue,目前仍然可以看到类似的 issue,官方貌似也还没办法解决,所以,如果您必须要使用代理,则可以参考后面的解决办法。

import selectors
import asyncio
import sys

if sys.platform == 'win32':
    loop = asyncio.ProactorEventLoop()
    asyncio.set_event_loop(loop)
else:
    selector = selectors.PollSelector()
    loop = asyncio.SelectorEventLoop(selector)
    asyncio.set_event_loop(loop)

2.限制并发量

可以使用方法 asyncio.Semaphore() 来限制并发量,Semaphore 就是信号量的意思,Semaphore 管理一个内部计数器,该计数器在每次调用 acquire() 方法时递减,每次调用 release() 方法时递增,计数器永远不会低于零,当方法 acquire() 发现它为零时,它会阻塞,等待其他线程调用 release() 方法。

通过限制并发量的方法来解决报错问题是个不错的选择。

import aiohttp
import asyncio


num = 0


async def main(url, semaphore):
    async with semaphore:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                global num
                num += 1
                print('%s ——> %s' % (str(num), response.status))


def tasks():
    semaphore = asyncio.Semaphore(300)                         # 限制并发量为 300
    url = 'https://www.baidu.com/s?ie=UTF-8&wd=%s'
    task = [main(url % i, semaphore) for i in range(10000)]    # #总共 10000 任务
    return task


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

3.修改最大文件描述符限制Windows

在 Windows 中,最大文件描述符限制在 C 语言的头文件 Winsock2.h 中使用变量 FD_SETSIZE 进行定义,如果要修改它,可以通过在包含 Winsock2.h 之前将 FD_SETSIZE 定义为另一个值来修改,如果我们使用的编程语言是 Python 的话,是不太好对这个值进行修改的,可以参考微软官方文档:https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select

Linux

在 Linux 平台,可以使用 ulimit 命令来修改最大文件描述符限制:

* hard nofile 65536
* soft nofile 65536

ulimit 命令参考:

总结

asyncio 事件循环选择器,在 Python 3.7 以及之前的版本中,所有平台默认使用的都是 SelectorEventLoop,在 Python 3.8 以及以后的版本中,Unix 平台默认使用的是 SelectorEventLoop,Windows 平台默认使用的是 ProactorEventLoop。

select 在 Windows 中限制了文件描述符最大数量为 512 个,在 Linux 中限制为 1024 个。

要解决 ValueError: too many file descriptors in select() 的报错问题,根据您的平台和业务要求选择合理的解决方法:

Windows

Linux

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文