Python解決asyncio文件描述符最大數(shù)量限制的問題
問題復(fù)現(xiàn)
Windows 平臺下,Python 版本 3.5,使用異步框架 asyncio,有時候會出現(xiàn) ValueError: too many file descriptors in select() 的報錯信息,我們就來聊一下為什么會出現(xiàn)這種問題,以及問題的一些解決方法。
寫一個小 dome 復(fù)現(xiàn)這個問題(環(huán)境: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 次左右后就會出現(xiàn)以下報錯:
問題分析
好像這個報錯和 select 有關(guān),那什么是 select 呢?要怎么解決呢?別急,我們首先來了解一下 asyncio 中的事件循環(huán),即 EventLoop。
事件循環(huán) EventLoop
事件循環(huán)是 asyncio 的核心,異步任務(wù)的運(yùn)行、任務(wù)完成之后的回調(diào)、網(wǎng)絡(luò) I/O 操作、子進(jìn)程的運(yùn)行,都是通過事件循環(huán)完成的,通俗來講,事件循環(huán)所做的就是等待事件發(fā)生,然后再將每個事件與我們已明確與所述事件類型匹配的函數(shù)進(jìn)行匹配。
下圖很好的展示了協(xié)程、事件循環(huán)之間的相互作用:
在 asyncio 中,主要提供了兩種不同事件循環(huán)的實現(xiàn)方法:
- SelectorEventLoop:基于 selectors 模塊的事件循環(huán),selectors 又是建立在底層的 I/O 復(fù)用模塊 select 之上的,selectors 提供了高度封裝和高效的 I/O 復(fù)用,也就是說 SelectorEventLoop 在底層就是使用了 select I/O 多路復(fù)用的機(jī)制。
- ProactorEventLoop:使用 IOCP 專為 Windows 構(gòu)建的事件循環(huán),IOCP 全稱 I/O Completion Port,即 I/O 完成端口。它是支持多個同時發(fā)生的異步 I/O 操作的應(yīng)用程序編程接口,它充分利用內(nèi)核對象的調(diào)度,只使用少量的幾個線程來處理和客戶端的所有通信,消除了無謂的線程上下文切換,是 Windows 下性能最好的 I/O 模型,有關(guān) IOCP 的詳細(xì)介紹可參考微軟文檔。
那么這兩種方法有什么區(qū)別呢?在 asyncio 中什么時候用什么方法呢?
我們不妨看一下 asyncio 的源碼,在 Python 3.7 中,無論在 Windows 還是 Linux 中都可以看到其默認(rèn)的設(shè)置是 SelectorEventLoop:
我們也可以分別在 Windows 平臺和 Linux 平臺打印一下 EventLoop 對象(Python 3.7),可以看到默認(rèn)都是 SelectorEventLoop:
import asyncio loop = asyncio.get_event_loop() print(loop)
- Windows:
- Linux:
事實上,在 Python 3.7 以及之前的版本中, 所有平臺默認(rèn)使用的都是 SelectorEventLoop,在 Python 3.8 以及以后的版本中,Unix 平臺默認(rèn)使用的是 SelectorEventLoop,Windows 平臺默認(rèn)使用的是 ProactorEventLoop,這個差異可以在官方文檔中看到。
- Python 3.7 文檔:https://docs.python.org/3.7/library/asyncio-eventloop.html#event-loop-implementations
- Python 3.8 文檔:https://docs.python.org/3.8/library/asyncio-eventloop.html#event-loop-implementations
說了這么多,這和 ValueError: too many file descriptors in select()
的報錯問題有什么關(guān)系呢?select 到底是什么東西呢?
I/O 多路復(fù)用
要了解 select,我們還要了解一下什么是 I/O 多路復(fù)用(I/O multiplexing),服務(wù)器端編程經(jīng)常需要構(gòu)造高性能的 I/O 模型,常見的 I/O 模型有同步阻塞 I/O、同步非阻塞 I/O、I/O 多路復(fù)用等;當(dāng)需要同時處理多個客戶端接入請求時,可以利用多線程或者 I/O 多路復(fù)用技術(shù)進(jìn)行處理,I/O 多路復(fù)用技術(shù)就是為了解決進(jìn)程或線程阻塞到某個 I/O 系統(tǒng)調(diào)用而出現(xiàn)的技術(shù),使進(jìn)程不阻塞于某個特定的 I/O 系統(tǒng)調(diào)用。
select,poll,epoll 等都是 I/O 多路復(fù)用的一種機(jī)制,其中后兩個在 Linux 中可用,Windows 僅支持 select,I/O 多路復(fù)用通過這種機(jī)制,可以監(jiān)視多個描述符,一旦某個描述符就緒,一般是讀就緒或者寫就緒,就是在這個文件描述符進(jìn)行讀寫操作之前,能夠通知程序進(jìn)行相應(yīng)的讀寫操作。
select 的缺點(diǎn)
I/O 多路復(fù)用這個概念被提出來以后, select 是第一個實現(xiàn)這個概念的,select 被實現(xiàn)以后,很快就暴露出了很多問題,其中一個缺點(diǎn)就是 select 在 Windows 中限制了文件描述符數(shù)量為 512 個,在 Linux 中限制為 1024 個,那么在前面的 dome 中,使用的是 Python 3.5,這個版本的 asyncio 默認(rèn)使用了 SelectorEventLoop,底層調(diào)用的是 select,受 select 缺點(diǎn)的影響,并發(fā)量過高,就出現(xiàn)了 ValueError: too many file descriptors in select() 的報錯信息。
解決方法
1.更換事件循環(huán)選擇器
如果你使用的是 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.限制并發(fā)量
可以使用方法 asyncio.Semaphore()
來限制并發(fā)量,Semaphore 就是信號量的意思,Semaphore 管理一個內(nèi)部計數(shù)器,該計數(shù)器在每次調(diào)用 acquire()
方法時遞減,每次調(diào)用 release()
方法時遞增,計數(shù)器永遠(yuǎn)不會低于零,當(dāng)方法 acquire()
發(fā)現(xiàn)它為零時,它會阻塞,等待其他線程調(diào)用 release()
方法。
通過限制并發(fā)量的方法來解決報錯問題是個不錯的選擇。
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) # 限制并發(fā)量為 300 url = 'https://www.baidu.com/s?ie=UTF-8&wd=%s' task = [main(url % i, semaphore) for i in range(10000)] # #總共 10000 任務(wù) return task loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait(tasks()))
3.修改最大文件描述符限制Windows
在 Windows 中,最大文件描述符限制在 C 語言的頭文件 Winsock2.h 中使用變量 FD_SETSIZE
進(jìn)行定義,如果要修改它,可以通過在包含 Winsock2.h 之前將 FD_SETSIZE
定義為另一個值來修改,如果我們使用的編程語言是 Python 的話,是不太好對這個值進(jìn)行修改的,可以參考微軟官方文檔:https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
Linux
在 Linux 平臺,可以使用 ulimit
命令來修改最大文件描述符限制:
- 查看當(dāng)前會話最大文件描述符限制(默認(rèn)1024):
ulimit -n
- 臨時修改限制,只對當(dāng)前的會話有效:
ulimit -SHn 65536
- 永久修改限制,在
/etc/security/limits.conf
文件里新增以下內(nèi)容:
* hard nofile 65536 * soft nofile 65536
ulimit
命令參考:
- -S使用軟 (soft) 資源限制
- -H使用硬 (hard) 資源限制
- -a所有當(dāng)前限制都被報告
- -b套接字緩存尺寸
- -c創(chuàng)建的核文件的最大尺寸
- -d一個進(jìn)程的數(shù)據(jù)區(qū)的最大尺寸
- -e最高的調(diào)度優(yōu)先級 (nice)
- -f有 shell 及其子進(jìn)程可以寫的最大文件尺寸
- -i最多的可以掛起的信號數(shù)
- -k分配給此進(jìn)程的最大 kqueue 數(shù)量
- -l一個進(jìn)程可以鎖定的最大內(nèi)存尺寸
- -m最大的內(nèi)存進(jìn)駐尺寸
- -n最多的打開的文件描述符個數(shù)
- -p管道緩沖區(qū)尺寸
- -qPOSIX 信息隊列的最大字節(jié)數(shù)
- -r實時調(diào)度的最大優(yōu)先級
- -s最大棧尺寸
- -t最大的CPU時間,以秒為單位
- -u最大用戶進(jìn)程數(shù)
- -v虛擬內(nèi)存尺寸
- -x最大的文件鎖數(shù)量
- -P最大偽終端數(shù)量
- -T最大線程數(shù)量
總結(jié)
asyncio 事件循環(huán)選擇器,在 Python 3.7 以及之前的版本中,所有平臺默認(rèn)使用的都是 SelectorEventLoop,在 Python 3.8 以及以后的版本中,Unix 平臺默認(rèn)使用的是 SelectorEventLoop,Windows 平臺默認(rèn)使用的是 ProactorEventLoop。
select 在 Windows 中限制了文件描述符最大數(shù)量為 512 個,在 Linux 中限制為 1024 個。
要解決 ValueError: too many file descriptors in select()
的報錯問題,根據(jù)您的平臺和業(yè)務(wù)要求選擇合理的解決方法:
Windows
- 通過
asyncio.Semaphore()
方法來限制并發(fā)量,通常設(shè)置在 300-500 比較合理,這是最優(yōu)的做法; - 更換 asyncio 的事件循環(huán)選擇器為 ProactorEventLoop,注意:這將導(dǎo)致無法使用代理!
Linux
- 通過
asyncio.Semaphore()
方法來限制并發(fā)量,通常設(shè)置在 800-1000 比較合理; - 通過
ulimit
命令來修改最大文件描述符限制; - 更換 asyncio 的事件循環(huán)選擇器為 PollSelector。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
python運(yùn)行cmd命令行的3種方法總結(jié)
雖然python在調(diào)用cmd命令方面使用的比較少,不過還是要用的,下面這篇文章主要給大家介紹了關(guān)于python運(yùn)行cmd命令行的3種方法,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09Python3中使用urllib的方法詳解(header,代理,超時,認(rèn)證,異常處理)
這篇文章整理了一些關(guān)于urllib使用中的一些關(guān)于header,代理,超時,認(rèn)證,異常處理處理方法,對大家學(xué)習(xí)python具有一定的參考借鑒價值,有需要的朋友們下面來一起看看吧。2016-09-09Python中使用ConfigParser解析ini配置文件實例
這篇文章主要介紹了Python中使用ConfigParser解析ini配置文件實例,本文給出了創(chuàng)建和讀取ini文件的例子,需要的朋友可以參考下2014-08-08python機(jī)器學(xué)習(xí)實現(xiàn)神經(jīng)網(wǎng)絡(luò)示例解析
這篇文章主要為大家介紹了python機(jī)器學(xué)習(xí)python實現(xiàn)神經(jīng)網(wǎng)絡(luò)的示例解析,在同樣在進(jìn)行python機(jī)器學(xué)習(xí)的同學(xué)可以借鑒參考下,希望能夠有所幫助2021-10-10Pytorch神經(jīng)網(wǎng)絡(luò)參數(shù)管理方法詳細(xì)講解
這篇文章主要介紹了Pytorch神經(jīng)網(wǎng)絡(luò)參數(shù)管理方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-05-05