Python中的Event Loop和多线程

目录

  1. 都怪GIL,threading不能加速CPU bound任务
  2. 对于I/O bound任务,选择在于I/O操作是否阻塞
  3. 并非所有event loop实现方案都是单线程实现
  4. 总结

这篇文章作为我对Python并发编程方案相关知识的思考、总结和陈述,希望这篇文章能帮到苦于理解和选择并发方案的其他人。对Python语言来说,有三种并发方案:多线程(threading)、多进程(multiprocessing)、event loop(asyncio etc.)。

#都怪GIL,threading不能加速CPU bound任务

在Python语言中只有multiprocessing库可以加速CPU bound任务。为什么threading库不可以?因为有GIL的存在,同一时间只有一个线程在运行(Python字节码)[1]。即使threading库(通过_thread库[2],进而通过诸如pthread等底层库[3])创建的是kernel-level thread(kernel-level thread可以被OS的scheduler调度在不同CPU core上执行[4])。这一点可以用以下代码验证(感谢perker,kky两位,我都想不到这种验证方法,学习了):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import threading
import time
import os

def worker():
    while True:
        time.sleep(1)
        
t = threading.Thread(target=worker, daemon=True)
t.start()

print(f"{os.getpid()}")

运行上述代码获取pid,在另一个终端执行ps -o thcount {pid},会看到结果是THCNT 2,一个主线程一个worker线程。

#对于I/O bound任务,选择在于I/O操作是否阻塞

本段落中所述也适用于其他编程语言。在event loop方案中,任务在遇到阻塞时马上交出执行权;而在多线程(kernel-level thread)方案中,OS的scheduler需要等待被I/O阻塞的线程跑完分配给该线程的时间片再调度运行其他线程。这样一想,似乎event loop方案在处理I/O bound任务时始终优于多线程方案。实际上选择哪个方案取决于你将要调用的I/O操作是否为阻塞,只要在计算流程上有一个调用是阻塞I/O,就只能选多线程方案了。

在event loop方案中,阻塞I/O会block住当前运行该逻辑的工作线程。我找到了一句对Pythonasyncio库的辛辣评论:

Async is just a pretty face on single-threaded callback hell. – Reddit user, Niicodemus [5]

这位的观点在上下文语境中是正确的。“a pretty face on callback hell”(真怀念,这是我求职时被问到的问题,下次我会这样回答),即Python的asyncawait关键字的一个作用是作为callback的语法糖[6]。asyncio库的event loop实现是单线程的[7],所以如果在使用asyncio库的同时调用阻塞I/O,整个都会卡住。最好调用loop.run_in_executor(executor, func, *args)方法,在线程池中运行阻塞I/O(或在进程池中运行CPU bound任务),将阻塞等待转换为非阻塞等待。

#并非所有event loop实现方案都是单线程实现

另外,Event loop结构在不同实现方案中处于不同的位置。

(In Netty,) The event loop runs continuously in a single thread, although we can have as many event loops as the number of available cores.[8]

Java生态下的Spring WebFlux作为一个web application框架,可使用多种web server作为底层依赖。其中Netty是一种实现了非阻塞I/O的web client/server 框架,提供TCP、UDP、HTTP协议的非阻塞实现。使用Netty编写web server时,需要声明两类线程,接收请求的线程(boss)和处理请求的线程(worker)。每一个线程中都存在一个event loop,被作为EventLoopGroup统一管理(因而我们要为一个web server声明bossGroupworkerGroup两个EventLoopGroup[9])。WebFlux库拥有类似asyncioloop.run_in_executor()方法的publishOn(Scheduler s)方法,将当前调用链的后续步骤移到另一个Scheduler(即另一个线程池)中运行。

JavaScript语言被定义为一个“单线程”语言[10],它应当被执行在单线程环境中(但是书面约定并不阻止你多线程执行JavaScript代码)。JavaScript引擎是负责解释并执行JavaScript代码的程序,大多数引擎(比如Chrome V8)遵守了单线程执行的约定(除了Rhino引擎[11])。如果你在前端JavaScript代码中调用阻塞I/O,由JavaScript事件驱动的UI将无法响应用户操作[12],因为event loop始终无法空闲下来并处理下一个事件。

Node.js uses a small number of threads to handle many clients. In Node.js there are two types of threads: one Event Loop (aka the main loop, main thread, event thread, etc.), and a pool of k Workers in a Worker Pool (aka the threadpool).[13]

Node.js基于V8引擎和libuv库,生成一个Worker Pool(基于一个kernel-level thread pool12,这个线程池是由libuv库调用系统调用申请创建的[14])来处理所有逻辑[15]。因此在Node.js所运行的代码中调用阻塞I/O的行为将仅仅阻塞当前运行这段逻辑的worker,其他worker仍将正常运行。但Node.js只有一个event loop调度全部worker。

#总结

对三种并发方案的选择变得很简单,简单到不会忘记:

  1. CPU bound -> 多进程
  2. I/O bound + Blocking I/O -> 多线程
  3. I/O bound + Non-blocking I/O -> Event Loop

实施event loop方案后遇到Blocking I/O或CPU bound任务,可以通过将任务运行在额外线程池/进程池中,将阻塞形式的结果变为非阻塞形式的结果,并使用正常的异步方式消费结果,框架或语言会提供这类转换API(如上文提到的asyncio库的loop.run_in_executor()WebFlux库的publishOn())。


  1. 而GIL之所以存在,是因为Python的内存管理机制不是线程安全的。 GlobalInterpreterLock - Python Wiki 

  2. _thread — Low-level threading API — Python 3.12.5 documentation

  3. 17.9. _thread — Low-level threading API — Python 3.6.15 documentation

  4. A thread-aware operating system schedules threads, not processes. Threads 

  5. https://old.reddit.com/r/django/comments/12lw2w8/mixing_sync_and_async_views_in_the_same/jgamyb1/

  6. https://iximiuz.com/en/posts/from-callback-hell-to-async-await-heaven/

  7. Event Loop — Python 3.12.5 documentation

  8. Concurrency in Spring WebFlux | Baeldung

  9. The second one, often called ‘worker’, handles the traffic of the accepted connection once the boss accepts the connection and registers the accepted connection to the worker. https://netty.io/wiki/user-guide-for-4.x.html#wiki-h3-5 

  10. node.js - When we say that the Javascript engine / nodejs is single threaded, does that mean it uses a single logical core in a processor and nothing more? - Stack Overflow

  11. concurrency - Is JavaScript guaranteed to be single-threaded? - Stack Overflow

  12. concurrency - Is JavaScript guaranteed to be single-threaded? - Stack Overflow  ↩︎

  13. javascript - Why is LIBUV needed in Node JS? - Stack Overflow

  14. javascript - Single Threaded Event Loop vs Multi Threaded Non Blocking Worker in Node.JS - Stack Overflow

  15. https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop#why-should-i-avoid-blocking-the-event-loop-and-the-worker-pool

Python中的Event Loop和多线程 | BoHolder的网站:博客,小玩意及其他

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值