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 主要特性有以下几点:
- 基于 libev 的快速事件循环,Linux上面的是 epoll 机制
- 基于 greenlet 的 轻量级执行单元
- API 复用了 Python 标准库里的内容。API 的概念和 Python 标准库一致(如事件,队列)。
- TCP / UDP / HTTP 服务器
- 支持 SSL 的协作式 sockets
- 子进程支持( 通过 gevent.subprocess )
- 线程池
- greenlets是确定性的。给定相同的绿色配置和相同的输入集,它们总是产生相同的输出
- gevent 每次遇到 io 操作,需要耗时等待时,会自动跳到下一个协程继续执行。
- gevent 的代码风格和线程非常相似,运行出来后的效果也非常相似。
- 通过 monkey patching 功能来使得第三方模块变成协作式
libevent 是一个事件分发引擎,greenlet 提供了轻量级线程的支持,gevent 就是基于这两个的一个专门处理网络逻辑的并行库。
- gevent.spawn 启动的所有协程,都是运行在同一个线程之中,所以协程不能跨线程同步数据。
- gevent.queue.Queue 是协程安全的。
- gevent 启动的并发协程,具体到 task function,不能有长时间阻塞的IO操作。因为 gevent 的协程的特点是,当前协程阻塞了才会切换到别的协程。如果当前协程长时间阻塞,则不能显示( gevent.sleep(0),或隐式,由gevent来做)切换到别的协程。导致程序出问题。
- 如果有长时间阻塞的 IO 操作,还是用传统的线程模型比较好。
- 因为 gevent 的特点总结是:事件驱动 + 协程 + 非阻塞IO,事件驱动指的是 libvent 对 epool 的封装,是基于事件的方式处理 IO。协程指的是 greenlet,非阻塞 IO 指的是 gevent 已经 patch 过的各种库,例如 socket 和 select 等等。
- 使用 gevent 的协程,最好要用 gevent 自身的非阻塞的库。如 httplib, socket, select 等等。
- 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。