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

进程、线程、协程区分

我们通常所说的协程Coroutine其实是corporate routine的缩写,直接翻译为协同的例程,一般我们都简称为协程。

在linux系统中,线程就是轻量级的进程,而我们通常也把协程称为轻量级的线程即微线程。

进程和协程

下面对比一下进程和协程的相同点和不同点:

相同点:

相同点存在于,当我们挂起一个执行流的时,我们要保存的东西:

  • 栈, 其实在你切换前你的局部变量,以及要函数的调用都需要保存,否则都无法恢复
  • 寄存器状态,这个其实用于当你的执行流恢复后要做什么

而寄存器和栈的结合就可以理解为上下文,上下文切换的理解:
CPU看上去像是在并发的执行多个进程,这是通过处理器在进程之间切换来实现的,操作系统实现这种交错执行的机制称为上下文切换

操作系统保持跟踪进程运行所需的所有状态信息。这种状态,就是上下文。
在任何一个时刻,操作系统都只能执行一个进程代码,当操作系统决定把控制权从当前进程转移到某个新进程时,就会进行上下文切换,即保存当前进程的上下文,恢复新进程的上下文,然后将控制权传递到新进程,新进程就会从它上次停止的地方开始。

不同点:

  • 执行流的调度者不同,进程是内核调度,而协程是在用户态调度,也就是说进程的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的,很显然用户态的代价更低
  • 进程会被强占,而协程不会,也就是说协程如果不主动让出CPU,那么其他的协程,就没有执行的机会。
  • 对内存的占用不同,实际上协程可以只需要4K的栈就足够了,而进程占用的内存要大的多
  • 从操作系统的角度讲,多协程的程序是单进程,单协程

线程和协程

既然我们上面也说了,协程也被称为微线程,下面对比一下协程和线程:

  • 线程之间需要上下文切换成本相对协程来说是比较高的,尤其在开启线程较多时,但协程的切换成本非常低。
  • 同样的线程的切换更多的是靠操作系统来控制,而协程的执行由我们自己控制。

  协程只是在单一的线程里不同的协程之间切换,其实和线程很像,线程是在一个进程下,不同的线程之间做切换,这也可能是协程称为微线程的原因吧。

Gevent模块

Gevent是一种基于协程的Python网络库,它用到Greenlet提供的,封装了libevent事件循环的高层同步API。它让开发者在不改变编程习惯的同时,用同步的方式写异步I/O的代码。

简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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),
])

  结果: 

1
2
3
4
12
56
34
78

猴子补丁 Monkey patching

这个补丁是Gevent模块最需要注意的问题,有了它,才会让Gevent模块发挥它的作用。我们往往使用Gevent是为了实现网络通信的高并发,但是,Gevent直接修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。但是我们无法保证你在复杂的生产环境中有哪些地方使用这些标准库会由于打了补丁而出现奇怪的问题。

一种方法是使用gevent下的socket模块,我们可以通过”from gevent import socket”来导入。不过更常用的方法是使用猴子布丁(Monkey patching)。使用猴子补丁褒贬不一,但是官网上还是建议使用”patch_all()”,而且在程序的第一行就执行。

1
2
3
4
5
6
7
8
9
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()”方法将所有标准库都替换。

获取协程状态

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

另外,greenlet协程运行过程中发生的异常是不会被抛出到协程外的,因此需要用协程对象的”exception”属性来获取协程中的异常。

下面的例子很好的演示了各种方法和属性的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/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参数来设置超时,我们也可以在全局范围内设置超时时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
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语句块中有效:

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

此外,我们可以指定超时所抛出的异常,来替换默认的”Timeout”异常。比如下例中超时就会抛出我们自定义的”TooLong”异常。

1
2
3
4
5
class TooLong(Exception):
    pass
  
with Timeout(1, TooLong):
    gevent.sleep(10)

协程间通信

事件(Event)对象

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/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作如下改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/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个消费者会分别消费队列中的产品,且消费过的产品不会被另一个消费者再取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/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就是释放。当所有信号量都已被获取,那剩余的协程就只能等待任一协程释放信号量后才能得以运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/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协程被调度。程序运行后的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
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,那就等同于同步锁。

协程本地变量

同线程类似,协程也有本地变量,也就是只在当前协程内可被访问的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/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模型

服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/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协程的方式,在每个客户端与服务器交互时,并不会影响到服务器端的工作。

  客户端: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/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()

Gevent模块,协程应用的更多相关文章

  1. python 并发编程 基于gevent模块 协程池 实现并发的套接字通信

    基于协程池 实现并发的套接字通信 客户端: from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('12 ...

  2. Python使用gevent实现协程

    Python中多任务的实现可以使用进程和线程,也可以使用协程. 一.协程介绍 协程,又称微线程.英文名Coroutine.协程是Python语言中所特有的,在其他语言中没有. 协程是python中另外 ...

  3. Python 通过gevent实现协程

    #coding:utf-8-*- '''协程(coroutine)又称微线程.纤程,是一种用户级的轻量级线程.协程有自己的寄存器上下文和栈.携程调度时,将寄存器上下文和栈 保存,在切换回来的时候恢复保 ...

  4. Gevent的协程实现原理

    之前之所以看greenlet的代码实现,主要就是想要看看gevent库的实现代码. .. 然后知道了gevent的协程是基于greenlet来实现的...所以就又先去看了看greenlet的实现... ...

  5. gevent实现协程

    gevent的好处:能够自动识别程序中的耗时操作,在耗时的时候自动切换到其他任务 # gevent的好处:能够自动识别程序中的耗时操作,在耗时的时候自动切换到其他任务 from gevent impo ...

  6. gevent多协程运用

    #导包 import gevent #猴子补丁 from gevent import monkey monkey.patch_all() from d8_db import ConnectMysql ...

  7. 线程回调,线程中的队列,事件,greenlet模块,gevent模块,自定义补丁, 单线程实现并发,协程

    1.线程回调 在线程池/进程池每次提交任务,都会返回一个表示任务的对象,Future对象Future对象具备一个绑定方法,add_done_callback 用于指定回调函数 add 意味着可以添加多 ...

  8. 协程gevent模块和猴子补丁

    # pip 装模块 greenlet和gevent # 协程 # 与进程.线程一样也是实现并发的手段 # 创建一个线程.关闭一个线程都需要创建寄存器.栈等.需要消耗时间 # 协程本质上是一个线程 # ...

  9. python并发编程之gevent协程(四)

    协程的含义就不再提,在py2和py3的早期版本中,python协程的主流实现方法是使用gevent模块.由于协程对于操作系统是无感知的,所以其切换需要程序员自己去完成. 系列文章 python并发编程 ...

  10. 什么是协程(第三方模块gevent--内置模块asyncio)

    目录 一:协程 1.什么是协程? 2.携程的作用? 3.安装第三方模块:在命令行下 二:greenlet模块(初级模块,实现了保存状态加切换) 三: gevent模块(协程模块) 1.time 模式协 ...

随机推荐

  1. JAVA 启动服务命令

    java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=7301,suspend=n -Xms2048m -Xmx4096m -XX:+ ...

  2. MySQL之自连接

    自连接就是说,在同一个表中,看做是两个表,下表表示 找每个人的领导,如果没有领导,显示无领导,eid 对应 leaderid,请看员工表 mysql> select * from emp; +- ...

  3. shell脚本sed的基本用法

    sed 我们首先准备了一个测试文件 1. s 替换  将文件中的This替换cyy 在替换的时候如果加入了 -i 选项就会真的替换,但是只会替换每一行的第一个 -n 和 -p 一起使用表示的是打印那些 ...

  4. BZOJ 3553: [Shoi2014]三叉神经树 LCT

    犯傻了,想到了如果是 0->1 的话就找最深的非 1 编号,是 1 -> 0 的话就找最深的非 0 编号. 但是没有想到这个东西可以直接维护. 假设不考虑叶子节点,那么如果当前点的值是 1 ...

  5. 使用rrweb 进行web 操作录制以及回放

    rrweb 是使用typescript 开发的web 操作录制以及回放框架,包含了比较完整的系统组件 rrweb-snapshot 进行dom 与操作实践的关联处理 rrweb 主要包含了record ...

  6. [RN] React Native 使用 FlatList 实现九宫格布局 GridList

    React Native 使用 FlatList 实现九宫格布局 先看图片演示实例: 本文以图片列表为例,实现九宫格布局! 主要有两种方法: 1)方法一: 利用FlatList的 numColumns ...

  7. Cocos Creator打包发布

    若本号内容有做得不到位的地方(比如:涉及版权或其他问题),请及时联系我们进行整改即可,会在第一时间进行处理. 请点赞!因为你们的赞同/鼓励是我写作的最大动力! 欢迎关注达叔小生的简书! 这是一个有质量 ...

  8. Redis BGSAVE因为内存不足 fork 失败导致目标 Redis 无法访问的问题

    中秋的时候正在外面愉快的在外卖喝着咖啡玩电脑......突发 redis 报警从 sentry 应用端曝出的错误 MISCONF Redis is configured to save RDB sna ...

  9. Spring Boot YML 掀翻 Properties!!

    .properties 配置文件大家应该都很熟悉,键值对嘛,.yml 配置文件栈长也是从 Spring Boot 开始了解到的. 那么,这两种格式的配置文件到底有哪些区别呢?哪个更好?能不能替换代替? ...

  10. bean名称相同冲突Annotation-specified bean name 'xx' for bean class [xxx] conflicts with existing, non-compatible bean definition of same name and class[xxx]

    工程中引入其他工程的包,由于两个工程中有重名的两个bean,导致在启动时提示如下错误: 根据bean名称在ide中查找,找到这两个重名的类,可以看到由于这两个类使用@Service标注,此时如果不使用 ...