IO 密集型應用IO 密集型應用CPU等待IO時間遠大於CPU 自身運行時間,太浪費;常見的 IO 密集型業務包括:瀏覽器交互、磁盤請求、網絡爬蟲、數據庫請求等
Python 世界對於 IO 密集型場景的併發提升有 3 種方法:多進程、多線程、異步 IO(asyncio);理論上講asyncio是性能最高的,原因如下:
1.進程、線程會有CPU上下文切換
2.進程、線程需要內核態和用戶態的交互,性能開銷大;而協程對內核透明的,只在用戶態運行
3.進程、線程並不可以無限創建,最佳實踐一般是 CPU*2;而協程併發能力強,併發上限理論上取決於操作系統IO多路複用(Linux下是 epoll)可註冊的文件描述符的極限
那asyncio的實際表現是否如理論上那麼強,到底強多少呢?我構建瞭如下測試場景:
訪問500臺 DB,並sleep 100ms模擬業務查詢
方法 1;順序串行一臺臺執行
方法 2:多進程
方法 3:多線程
方法 4:asyncio
方法 5:asyncio+uvloop
最後的 asyncio+uvloop 和官方asyncio 最大不同是用 Cython+libuv 重新實現了asyncio 的事件循環(event loop)部分,官方測試性能是 node.js的 2 倍,持平 golang。
以下測試代碼需要 Pyhton3.7+: 順序串行一臺臺執行
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import records
user=xx
pass=xx
port=xx
hosts= [....] # 500臺 db列表
def query(host):
conn = records.Database(
f'mysql+pymysql://{user}:{pass}@{host}:{port}/mysql?charset=utf8mb4')
rows = conn.query('select sleep(0.1);')
print(rows[0])
def main():
for h in hosts:
query(h)
# main entrance
if __name__ == '__main__':
main()
多進程
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from concurrent import futures
import records
user=xx
pass=xx
port=xx
hosts= [....] # 500臺 db列表
def query(host):
conn = records.Database(
f'mysql+pymysql://{user}:{pass}@{host}:{port}/mysql?charset=utf8mb4')
rows = conn.query('select sleep(0.1);')
print(rows[0])
def main():
with futures.ProcessPoolExecutor() as executor:
for future in executor.map(query,hosts):
pass
# main entrance
if __name__ == '__main__':
main()
多線程
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from concurrent import futures
import records
user=xx
pass=xx
port=xx
hosts= [....] # 500臺 db列表
def query(host):
conn = records.Database(
f'mysql+pymysql://{user}:{pass}@{host}:{port}/mysql?charset=utf8mb4')
rows = conn.query('select sleep(0.1);')
print(rows[0])
def main():
with futures.ThreadPoolExecutor() as executor:
for future in executor.map(query,hosts):
pass
# main entrance
if __name__ == '__main__':
main()
asyncio
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
from databases import Database
user=xx
pass=xx
port=xx
hosts= [....] # 500臺 db列表
async def query(host):
DATABASE_URL = f'mysql+pymysql://{user}:{pass}@{host}:{port}/mysql?charset=utf8mb4'
async with Database(DATABASE_URL) as database:
query = 'select sleep(0.1);'
rows = await database.fetch_all(query=query)
print(rows[0])
async def main():
tasks = [asyncio.create_task(query(host)) for host in hosts]
await asyncio.gather(*tasks)
# main entrance
if __name__ == '__main__':
asyncio.run(main())
asyncio+uvloop
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import asyncio
import uvloop
from databases import Database
user=xx
pass=xx
port=xx
hosts= [....] # 500臺 db列表
async def query(host):
DATABASE_URL = f'mysql+pymysql://{user}:{pass}@{host}:{port}/mysql?charset=utf8mb4'
async with Database(DATABASE_URL) as database:
query = 'select sleep(0.1);'
rows = await database.fetch_all(query=query)
print(rows[0])
async def main():
tasks = [asyncio.create_task(query(host)) for host in hosts]
await asyncio.gather(*tasks)
# main entrance
if __name__ == '__main__':
uvloop.install()
asyncio.run(main())
運行時間對比
方式運行時間串行1m7.745s多進程2.932s多線程4.813sasyncio1.068sasyncio+uvloop0.750s
可以看出: 無論多進程、多進程還是asyncio都能大幅提升IO 密集型場景下的併發,但asyncio+uvloop性能最高,運行時間只有原始串行運行時間的 1/90,相差快 2 個數量級了!
內存佔用對比 串行
多進程
多線程
asyncio
asyncio+uvloop
可以看出 asyncio+uvloop 內存佔用表現仍然最優,只有 60M;而多進程佔用多達 1.4G,果然創建進程是個十分重的操作~
總結asyncio 無論運行時間還是內存佔用都遠優於多進程、多線程,配合 uvloop 性能還能進一步提升,在 IO 密集型業務中可以優先使用 asyncio。