Python 的 Gevent --- 高性能的 Python 并发框架

Gevent是一个基于libev和Greenlet的高性能Python并发框架,它提供了轻量级的Greenlets来实现协程,通过猴子补丁使得标准库支持协作式调度。Gevent使用事件驱动和非阻塞IO,适用于处理大量并发任务,尤其适合网络IO密集型应用。它支持TCP/UDP/HTTP服务器、子进程、线程局部变量、数据结构如事件、队列、锁等。Gevent通过greenlet.spawn()启动协程,使用gevent.joinall()进行并发执行。猴子补丁功能允许在不改变代码的情况下将阻塞式调用转换为协作式运行。Gevent的使用包括创建Greenlets、管理并发、超时控制、线程安全的数据结构等,适用于Web服务、流式处理等场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

From:http://www.xuebuyuan.com/1604603.html
Gevent 指南(英文):http://sdiehl.github.io/gevent-tutorial
Gevent 指南(中文):http://xlambda.com/gevent-tutorial
Gevent 指南(中文)下载地址:http://download.csdn.net/download/freeking101/9924351
初试Gevent – 高性能的 Python 并发框架:http://python.jobbole.com/87041

Python 开发使用 Gevent:http://sdiehl.github.io/gevent-tutorial/

greenlet、Eventlet,gevent 推荐 )、

  • greenlet :轻量级的并行编程,调度麻烦,用生成器实现的协程而且不是真正意义上的协程,只是实现代码执行过程中的挂起,唤醒操作。Greenlet 没有自己的调度过程,所以一般不会直接使用。greenlet:http://greenlet.readthedocs.org/en/latest/
  • eventlet:是在 greenlet 的基础上实现了自己的 GreenThread,实际上就是 greenlet 类的扩展封装,而与Greenlet的不同是,Eventlet实现了自己调度器称为Hub,Hub类似于Tornado的IOLoop,是单实例的。在Hub中有一个event loop,根据不同的事件来切换到对应的GreenThread。同时 Eventlet 还实现了一系列的补丁来使 Python 标准库中的 socket 等等module 来支持 GreenThread 的切换。Eventlet 的 Hub 可以被定制来实现自己调度过程。eventlet 目前支持 CPython 2.7 和 3.4+ 并将在未来删除,仅保留 CPython 3.5+ 支持。
  • Gevent:基于 libev 与 Greenlet 实现。不同于 Eventlet 的用 python 实现的 hub 调度,Gevent 通过 Cython 调用 libev 来实现一个高效的 event loop 调度循环。同时类似于 Event,Gevent 也有自己的 monkey_patch,在打了补丁后,完全可以使用 python 线程的方式来无感知的使用协程,减少了开发成本。 gevent 官网文档:http://www.gevent.org/contents.html

Gevent 主要特性有以下几点:

  1. 基于 libev 的快速事件循环,Linux上面的是 epoll 机制
  2. 基于 greenlet 的 轻量级执行单元
  3. API 复用了 Python 标准库里的内容。API 的概念和 Python 标准库一致(如事件,队列)。
  4. TCP / UDP / HTTP 服务器 
  5. 支持 SSL 的协作式 sockets
  6. 子进程支持( 通过 gevent.subprocess )
  7. 线程池
  8. greenlets是确定性的。给定相同的绿色配置和相同的输入集,它们总是产生相同的输出
  9. gevent 每次遇到 io 操作,需要耗时等待时,会自动跳到下一个协程继续执行。
  10. gevent 的代码风格和线程非常相似,运行出来后的效果也非常相似。
  11. 通过 monkey patching 功能来使得第三方模块变成协作式

libevent 是一个事件分发引擎,greenlet 提供了轻量级线程的支持,gevent 就是基于这两个的一个专门处理网络逻辑的并行库。

  1. gevent.spawn 启动的所有协程,都是运行在同一个线程之中,所以协程不能跨线程同步数据。
  2. gevent.queue.Queue 是协程安全的。
  3. gevent 启动的并发协程,具体到 task function,不能有长时间阻塞的IO操作。因为 gevent 的协程的特点是,当前协程阻塞了才会切换到别的协程。如果当前协程长时间阻塞,则不能显示( gevent.sleep(0),或隐式,由gevent来做)切换到别的协程。导致程序出问题。
  4. 如果有长时间阻塞的 IO 操作,还是用传统的线程模型比较好。
  5. 因为 gevent 的特点总结是:事件驱动 + 协程 + 非阻塞IO,事件驱动指的是 libvent 对 epool 的封装,是基于事件的方式处理 IO。协程指的是 greenlet,非阻塞 IO 指的是 gevent 已经 patch 过的各种库,例如 socket 和 select 等等。
  6. 使用 gevent 的协程,最好要用 gevent 自身的非阻塞的库。如 httplib, socket, select 等等。
  7. gevent 适合处理大量无阻塞的任务,如果有实在不能把阻塞的部分变为非阻塞再交给 gevent 处理,就把阻塞的部分改为异步吧。

原理:程序的重要部分是将任务函数封装到 gevent.spawn

  • 初始化的 greenlet 列表存放在数组 threads 中,此数组被传给 gevent.joinall 函数,
  • gevent.joinall 会阻塞当前流程,并执行所有给定的 greenlet,执行流程只会在所有 greenlet 执行完后才会继续向下走。 

gevent 实现了python 标准库里面大部分的阻塞式系统调用,包括 socket、ssl、threading 和 select 等模块,而将这些阻塞式调用变为协作式运行(参见猴子补丁)。

猴子补丁 Monkey Patch: 

  • (1)猴子补丁的由来 。猴子补丁的这个叫法起源于 Zope 框架,大家在修正 Zope 的 Bug 的时候经常在程序后面追加更新部分,这些被称作是 “杂牌军补丁(guerillapatch)”,后来 guerilla 就渐渐的写成了 gorllia(猩猩),再后来就写了 monkey(猴子),所以猴子补丁的叫法是这么莫名其妙的得来的。 后来在动态语言中,不改变源代码而对功能进行追加和变更,统称为 "猴子补丁"。所以猴子补丁并不是 Python 中专有的。猴子补丁这种东西充分利用了动态语言的灵活性,可以对现有的语言Api 进行追加,替换,修改 Bug,甚至性能优化等等。 使用猴子补丁的方式,gevent 能够修改标准库里面大部分的阻塞式系统调用,包括 socket、ssl、threading 和 select 等模块,而变为协作式运行。也就是通过猴子补丁的 monkey.patch_xxx() 来将 python 标准库中 模块 或 函数 改成 gevent 中的响应的具有协程的协作式对象。这样在不改变原有代码的情况下,将应用的阻塞式方法,变成协程式的。 
  • (2)猴子补丁使用时的注意事项 。猴子补丁的功能很强大,但是也带来了很多的风险,尤其是像 gevent 这种直接进行 API替换的补丁,整个 Python 进程所使用的模块都会被替换,可能自己的代码能 hold 住,但是其它第三方库,有时候问题并不好排查,即使排查出来也是很棘手,所以,就像松本建议的那样,如果要使用猴子补丁,那么只是做功能追加,尽量避免大规模的 API 覆盖。 虽然猴子补丁仍然是邪恶的(evil),但在这种情况下它是 "有用的邪恶(useful evil)"

Linux 的 epoll 和 libev

  • Linux 的 epoll 机制:epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 poll,是 Linux 下 "多路复用IO" select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。epoll 的优点:支持一个进程打开大数目的 socket 描述符。select 的一个进程所打开的 FD 由FD_SETSIZE 的设置来限定,而 epoll 没有这个限制,它所支持的 FD 上限是最大可打开文件的数目,远大于2048,而且IO 效率不随 FD 数目增加而线性下降:由于 epoll 只会对 "活跃" 的 socket 进行操作,于是,只有 "活跃" 的 socket 才会主动去调用 callback 函数,其他 idle 状态的 socket 则不会。epoll 使用 mmap 加速内核与用户空间的消息传递。epoll 是通过内核于用户空间 mmap 同一块内存实现的。
  • libev 机制:提供了指定文件描述符事件发生时调用回调函数的机制。libev是一个事件循环器:向libev注册感兴趣的事件,比如socket可读事件,libev会对所注册的事件的源进行管理,并在事件发生时触发相应的程序。

示例:

import gevent
from gevent import socket

urls = ['www.baidu.com', 'www.example.com', 'www.python.org']

jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]

gevent.joinall(jobs, timeout=2)

result = [job.value for job in jobs]
print(result)

结果:['61.135.169.125', '93.184.216.34', '151.101.228.223']

说明:gevent.spawn() 方法 spawn 一些 jobs,然后通过 gevent.joinall 将 jobs 加入到 微线程 执行队列中等待其完成,设置超时为 2 秒。执行后的结果通过检查 gevent.Greenlet.value 值来收集。gevent.socket.gethostbyname() 函数与标准的socket.gethotbyname() 有相同的接口,但它不会阻塞整个解释器,因此会使得其他的 greenlets 跟随着无阻的请求而执行。

##################################
看看 Gevent :What is gevent? — gevent 21.12.1.dev0 documentation 
您可以创建几个 Greenlet 对象为几个任务。
每个 greenlet 是 绿色的线程 :https://en.wikipedia.org/wiki/Green_threads

import gevent
from gevent import monkey
from gevent import Greenlet


monkey.patch_all()


class Task(Greenlet):
    def __init__(self, name):
        Greenlet.__init__(self)
        self.name = name

    def _run(self):
        print("Task %s: some task..." % self.name)


t1 = Task("task1")
t2 = Task("task2")
t1.start()
t2.start()
# here we are waiting all tasks
gevent.joinall([t1, t2])

示例代码:

from gevent import monkey; monkey.patch_all()
import gevent
import requests


def get_url(url):
    res = requests.get(url)
    print(url, res.status_code, len(res.text))


url_l = [
    'http://www.baidu.com',
    'http://www.python.org',
    'http://www.cnblogs.com'
]
g_l = []
for i in url_l:
    g_l.append(gevent.spawn(get_url, i))
gevent.joinall(g_l)

示例代码( 利用 gevent 并发抓取 ):

from gevent import monkey

monkey.patch_all()

import requests
import gevent
import io
import sys

# 解决console显示乱码的编码问题
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')


class Douban(object):
    """A class containing interface test method of Douban object"""

    def __init__(self):
        self.host = 'movie.douban.com'
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0',
            'Referer': 'https://movie.douban.com/',
        }

    def get_response(self, url, data):
        resp = requests.post(url=url, data=data, headers=self.headers).content.decode('utf-8')
        return resp

    def test_search_tags_movie(self):
        method = 'search_tags'
        url = 'https://%s/j/%s' % (self.host, method)
        post_data = {
            'type': 'movie',
            'source': 'index'
        }
        resp = self.get_response(url=url, data=post_data)
        print(resp)
        return resp


if __name__ == '__main__':
    douban = Douban()
    threads = []
    for i in range(6):
        thread = gevent.spawn(douban.test_search_tags_movie)
        threads.append(thread)

    gevent.joinall(threads)

并发爬图片:

from gevent import monkey
monkey.patch_all()
import requests
import gevent
from lxml import etree


def downloader(img_name, img_url):
    req = requests.get(img_url)
    img_content = req.content
    with open(img_name, "wb") as f:
        f.write(img_content)


def main():
    r = requests.get('http://www.nsgirl.com/portal.php')
    if r.status_code == 200:
        img_src_xpath = '//div[@id="frameXWswSe"]//div[@class="portal_block_summary"]//li//img/@src'
        s_html = etree.HTML(text=r.text)
        all_img_src = s_html.xpath(img_src_xpath)

        count = 0
        for img_src in all_img_src:
            count += 1
            # print(img_src)
            # http://www.nsgirl.com/forum.php?mod=image&aid=342&size=218x285&key=cd6828baf05c305c
            url = 'http://www.nsgirl.com/' + img_src
            gevent.joinall(
                [gevent.spawn(downloader, f"{count}.jpg", url), ]
            )


if __name__ == '__main__':
    main()

示例:

# -*- coding: utf-8 -*-

from gevent import monkey;

monkey.patch_all()
import gevent
import requests
from datetime import datetime


def f(url):
    print(f'time: {datetime.now()}, GET: {url}')
    resp = requests.get(url)
    print(f'time: {datetime.now()}, {len(resp.text)} bytes received from {url}.')


gevent.joinall([
    gevent.spawn(f, 'https://www.python.org/'),
    gevent.spawn(f, 'https://www.yahoo.com/'),
    gevent.spawn(f, 'https://github.com/'),
])

gevent 实现 生产者 - 消费者

# -*- coding: utf-8 -*-

from gevent import monkey

# 猴子补丁,all是所有能切换协程的地方都切换,包含了socket,所以一般都用all
monkey.patch_all()
from gevent.queue import Queue  # 队列 gevent中的队列
import gevent
import random

task_queue = Queue(3)


def producer(index=1):
    while True:
        print(f'生产者 [{index}]', end='')
        item = random.randint(0, 99)
        task_queue.put(item)
        print(f"生产 ---> {item}")


def consumer(index=1):
    while True:
        print(f'消费者 [{index}]', end='')
        item = task_queue.get()
        print(f"消费 ---> {item}")


def main_1():
    thread_1 = gevent.spawn(producer)
    thread_2 = gevent.spawn(consumer)
    thread_3 = gevent.spawn(consumer, 2)
    thread_list = [thread_1, thread_2, thread_3]
    gevent.joinall(thread_list)


if __name__ == '__main__':
    main_1()
    # main_2()
    pass

gevent 程序员指南

由 Gevent 社区编写

gevent是一个基于 libev的并发库。它为各种并发和网络相关的任务提供了整洁的API。

介绍

本指南假定读者有中级Python水平,但不要求有其它更多的知识,不期待读者有 并发方面的知识。本指南的目标在于给予你需要的工具来开始使用gevent,帮助你 驯服现有的并发问题,并从今开始编写异步应用程序。

gevent 官方文档还是可以看看的,尤其是源码里的 examples 都相当不错,有助于理解 gevent 的使用。gevent 封装了很多很方便的接口,其中一个就是 monkey。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值