Python gevent协程模块详解:从原理到并发实战

陆景行
陆景行
发布时间:2026-10-13 | 3人浏览过
原创

本文深入解析Python gevent模块,阐述协程与进程、线程的区别及上下文切换机制。重点讲解猴子补丁的使用、协程状态监控、超时控制、Event与Queue通信机制,以及基于BoundedSemaphore的信号量控制和协程本地变量。最后通过多并发Socket服务器与客户端示例,展示gevent在高并发网络编程中的实际应用。

Gevent官方文档地址:http://www.gevent.org/contents.html

进程、线程与协程的区别

通常所说的协程(Coroutine)源自corporate routine的缩写,直译为协同例程,一般简称为协程。

在Linux系统中,线程被视为轻量级进程,而协程则常被称为轻量级线程或微线程。

进程与协程的对比

以下对比进程和协程的异同点:

相同点:

  1. 当挂起一个执行流时,需要保存的内容包括:
  2. 栈空间:切换前需保存局部变量及函数调用链,否则无法恢复执行。

寄存器状态:用于记录执行流恢复后需执行的操作。

寄存器与栈的结合构成了上下文。上下文切换的理解如下: CPU看似并发执行多个进程,实则是通过处理器在进程间切换实现的。操作系统维持这种交错执行的机制称为上下文切换。

操作系统跟踪进程运行所需的所有状态信息,即上下文。 任一时刻,操作系统仅能执行一个进程代码。当决定将控制权从当前进程转移至新进程时,需进行上下文切换:保存当前进程上下文,恢复新进程上下文,并将控制权移交新进程,使其从上次停止处继续执行。

不同点:

  1. 执行流调度者不同:进程由内核调度,协程在用户态调度。进程上下文在内核态保存恢复,协程在用户态保存恢复,显然用户态代价更低。
  2. 抢占机制不同:进程会被强占,而协程不会。若协程不主动让出CPU,其他协程将无法执行。
  3. 内存占用不同:协程仅需约4K栈空间即可运行,而进程占用内存大得多。
  4. 从操作系统视角看,多协程程序表现为单进程、单协程。

线程与协程的对比

前文已述协程亦称微线程,以下对比协程与线程:

  1. 线程间上下文切换成本相对较高,尤其在开启大量线程时;而协程切换成本极低。
  2. 线程切换多由操作系统控制,而协程执行由开发者自行控制。

协程仅在单一线程内不同协程间切换,与线程在进程下不同线程间切换类似,这也是协程被称为微线程的原因。

Gevent模块概述

Gevent是基于协程的Python网络库,利用Greenlet提供的封装了libevent事件循环的高层同步API。它使开发者无需改变编程习惯,即可用同步方式编写异步I/O代码。

简单示例:

import gevent
def test1():
  print 12
  gevent.sleep(0)
  print 34
def test2():
  print 56
  gevent.sleep(0)
  print 78
gevent.joinall([
  gevent.spawn(test1),
  gevent.spawn(test2),
]) 

结果:

12 56 34 78

猴子补丁(Monkey patching)

此补丁是Gevent模块的关键,使其发挥作用。我们通常使用Gevent实现网络通信的高并发,但Gevent直接修改标准库中大部分阻塞式系统调用(包括socket、ssl、threading和select等模块),使其变为协作式运行。然而,在复杂生产环境中,无法保证所有使用标准库的地方因打补丁而不出现异常问题。

一种方法是使用gevent下的socket模块,通过“from gevent import socket”导入。但更常用的方法是使用猴子补丁(Monkey patching)。尽管该方法褒贬不一,官网仍建议使用“patch_all()”,并在程序第一行执行。

from gevent import monkey; monkey.patch_socket()
import gevent
import socket
urls = ['www.baidu.com', 'www.gevent.org', 'www.python.org']
jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
gevent.joinall(jobs, timeout=5)
print [job.value for job in jobs] 

上述代码第一行对socket标准库打上猴子补丁,此后socket标准库中的类和方法均被替换为非阻塞式,其他代码无需修改,从而真正体现协程效率。Python中其他标准库也存在阻塞情况,gevent提供“monkey.patch_all()”方法替换所有标准库。

获取协程状态

  1. started属性/ready()方法:判断协程是否已启动。
  2. successful()方法:判断协程是否成功运行且未抛出异常。
  3. value属性:获取协程执行完毕后的返回值。

此外,greenlet协程运行中发生的异常不会抛出到协程外,因此需通过协程对象的“exception”属性获取协程内异常。

以下示例演示了各种方法和属性的使用。

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
import gevent
def win():
  return 'You win!'
def fail():
 raise Exception('You failed!')
winner = gevent.spawn(win)
loser = gevent.spawn(fail)
print(winner.started) # True
print(loser.started)  # True
# 在Greenlet中发生的异常,不会被抛到Greenlet外面。
# 控制台会打出Stacktrace,但程序不会停止
try:
  gevent.joinall([winner, loser])
except Exception as e:
  # 这段永远不会被执行
  print('This will never be reached')
print(winner.ready()) # True
print(loser.started)  # True
print(winner.value) # 'You win!'
print(loser.value)  # None
print('successful ',winner.successful()) # True
print('successful ',loser.successful())  # False
# 这里可以通过raise loser.exception 或 loser.get()
# 来将协程中的异常抛出
print(loser.exception)

协程运行超时控制

此前介绍过在“gevent.joinall()”方法中可传入timeout参数设置超时,也可在全局范围内设置超时时间:

import gevent
from gevent import Timeout
timeout = Timeout(2) # 2 seconds
timeout.start()
def wait():

  gevent.sleep(10) 
try:
  gevent.spawn(wait).join()
except Timeout:
  print('Could not complete')

上例中,将超时设为2秒,此后所有协程运行若超过两秒将抛出“Timeout”异常。也可将超时设置在with语句内,使该设置仅在with语句块中有效:

with Timeout(1):
  gevent.sleep(10)

此外,可指定超时所抛出的异常以替换默认的“Timeout”异常。如下例中超时将抛出自定义的“TooLong”异常。

class TooLong(Exception):
  pass
with Timeout(1, TooLong):
  gevent.sleep(10)

协程间通信

事件(Event)对象

greenlet协程间异步通讯可使用事件(Event)对象。该对象的“wait()”方法可阻塞当前协程,而“set()”方法可唤醒之前阻塞的协程。在以下示例中,5个waiter协程均等待事件evt,当setter协程在3秒后设置evt事件,所有waiter协程即被唤醒。

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
import gevent
from gevent.event import Event
evt = Event()

def setter():
  print 'Wait for me'
  gevent.sleep(3) # 3秒后唤醒所有在evt上等待的协程
  print "Ok, I'm done"
  evt.set() # 唤醒

def waiter():
  print "I'll wait for you"
  evt.wait() # 等待
  print 'Finish waiting'

gevent.joinall([
  gevent.spawn(setter),
  gevent.spawn(waiter),
  gevent.spawn(waiter),
  gevent.spawn(waiter),
  gevent.spawn(waiter),
  gevent.spawn(waiter)
])

AsyncResult事件

除Event事件外,gevent还提供AsyncResult事件,可在唤醒时传递消息。将上例中的setter和waiter作如下改动:

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
from gevent.event import AsyncResult
aevt = AsyncResult()

def setter():
  print 'Wait for me'
  gevent.sleep(3) # 3秒后唤醒所有在evt上等待的协程
  print "Ok, I'm done"
  aevt.set('Hello!') # 唤醒,并传递消息

def waiter():
  print("I'll wait for you")
  message = aevt.get() # 等待,并在唤醒时获取消息
  print 'Got wake up message: %s' % message

队列(Queue)

队列Queue的概念众所周知,可通过put和get方法存取队列元素。gevent的队列对象允许greenlet协程间安全访问。运行以下程序,可见3个消费者分别消费队列中的产品,且消费过的产品不会被另一消费者再次取到:

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron<br>
import gevent
from gevent.queue import Queue
products = Queue()
def consumer(name):
  #while not products.empty():
  while True:
    try:
      print('%s got product %s' % (name, products.get_nowait()))
      gevent.sleep(0)
    except gevent.queue.Empty:
      break
  print('Quit')

def producer():
  for i in range(1, 10):
    products.put(i)

gevent.joinall([
  gevent.spawn(producer),
  gevent.spawn(consumer, 'steve'),
  gevent.spawn(consumer, 'john'),
  gevent.spawn(consumer, 'nancy'),
])

注意:协程队列与线程队列相同,put和get方法均为阻塞式,它们均有非阻塞版本:put_nowait和get_nowait。若调用get方法时队列为空,不会抛出“gevent.queue.Empty”异常。只能使用get_nowait()方式使其抛出异常。

信号量

信号量可用于限制协程并发个数。它有两个方法:acquire和release。顾名思义,acquire即获取信号量,release即释放。当所有信号量均已被获取,剩余协程只能等待任一协程释放信号量后得以运行:

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
import gevent
from gevent.coros import BoundedSemaphore
sem = BoundedSemaphore(2)
def worker(n):
  sem.acquire()
  print('Worker %i acquired semaphore' % n)
  gevent.sleep(0)
  sem.release()
  print('Worker %i released semaphore' % n)
gevent.joinall([gevent.spawn(worker, i) for i in xrange(0, 6)])

上例中,初始化了“BoundedSemaphore”信号量,并将其个数定为2。因此同一时间,仅有两个worker协程被调度。程序运行后结果如下:

Worker 0 acquired semaphore Worker 1 acquired semaphore Worker 0 released semaphore Worker 1 released semaphore Worker 2 acquired semaphore Worker 3 acquired semaphore Worker 2 released semaphore Worker 3 released semaphore Worker 4 acquired semaphore Worker 4 released semaphore Worker 5 acquired semaphore Worker 5 released semaphore

若信号量个数为1,则等同于同步锁。

协程本地变量

与线程类似,协程也有本地变量,即仅在当前协程内可访问的变量:

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
import gevent
from gevent.local import local
data = local()
def f1():
  data.x = 1
  print data.x

def f2():
  try:
    print data.x
  except AttributeError:
    print 'x is not visible'

gevent.joinall([
  gevent.spawn(f1),
  gevent.spawn(f2)
])

通过将变量存放在local对象中,即可将其作用域限制在当前协程内。当其他协程尝试访问该变量时,将抛出异常。不同协程间可有重名的本地变量,且互不影响。因协程本地变量的实现,是将其存放在以“greenlet.getcurrent()”的返回为键值的私有命名空间内。

多并发Socket模型

服务器端:

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
import socket
import gevent
from gevent import socket, monkey
monkey.patch_all()
def server(port):
  s = socket.socket()
  s.bind(('0.0.0.0', port))
  s.listen(500)
  while True:
    cli, addr = s.accept()
    gevent.spawn(handle_request, cli)

def handle_request(conn):
  try:
    while True:
      data = conn.recv(1024)
      print("recv:", data)
      conn.send(data)
      if not data:
        conn.shutdown(socket.SHUT_WR)

  except Exception as ex:
    print(ex)
  finally:
    conn.close()
if __name__ == '__main__':
  server(8001)

当客户端连接上服务器端时,服务器端通过开辟一个协程与该客户端完成交互任务。同时,由于使用了Gevent协程方式,在每个客户端与服务器交互时,并不会影响服务器端的工作。

客户端:

#!/usr/bin/env python
# _*_ coding utf-8 _*_
#Author: aaron
import socket
HOST = 'localhost' # The remote host
PORT = 8001     # The same port as used by the server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
while True:
  msg = bytes(input(">>:"), encoding="utf8")
  s.sendall(msg)
  data = s.recv(1024)
  # print(data)
  print('Received', repr(data)) # repr 格式化输出
s.close() 

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表