Python中的协程(Coroutine)操作模块(greenlet、gevent)


Posted in Python onMay 30, 2022

一、协程介绍

协程:英文名Coroutine,是单线程下的并发,又称微线程,纤程。

协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。对比操作系统控制线程的切换,用户在单线程内控制协程的切换。

协程自己本身无法实现并发(甚至性能会降低),协程+IO切换性能提高。

1、介绍

通常程序中子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。

看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

2、举例

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'


def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()


c = consumer()
produce(c)

执行结果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator,把一个consumer传入produce后:

  • 首先调用c.send(None)启动生成器;
  • 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  • consumer通过yield拿到消息,处理,又通过yield把结果传回;
  • produce拿到consumer处理的结果,继续生产下一条消息;
  • produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:“子程序就是协程的一种特例。”

3、优点如下:

  • 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
  • 单线程内就可以实现并发的效果,最大限度地利用cpu

4、缺点如下:

  • 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
  • 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

5、总结协程特点:

  • 必须在只有一个单线程里实现并发
  • 修改共享数据不需加锁
  • 用户程序里自己保存多个控制流的上下文栈
  • 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

二、greenlet(绿叶)模块

如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦),而使用greenlet模块可以非常简单地实现这20个任务直接的切换。

1、安装模块

pip3 install greenlet

2、greenlet实现状态切换

单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。

from greenlet import greenlet


def eat(name):
    print('%s eat 1' % name)
    g2.switch('nick')
    print('%s eat 2' % name)
    g2.switch()


def play(name):
    print('%s play 1' % name)
    g1.switch()
    print('%s play 2' % name)


g1 = greenlet(eat)
g2 = greenlet(play)

g1.switch('nick')  # 可以在第一次switch时传入参数,以后都不需要

3、效率对比

greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2...如此,才能提高效率,这就用到了Gevent模块。

#顺序执行
import time
def f1():
    res=1
    for i in range(100000000):
        res+=i

def f2():
    res=1
    for i in range(100000000):
        res*=i

start=time.time()
f1()
f2()
stop=time.time()
print('run time is %s' %(stop-start)) #10.985628366470337

#切换
from greenlet import greenlet
import time
def f1():
    res=1
    for i in range(100000000):
        res+=i
        g2.switch()

def f2():
    res=1
    for i in range(100000000):
        res*=i
        g1.switch()

start=time.time()
g1=greenlet(f1)
g2=greenlet(f2)
g1.switch()
stop=time.time()
print('run time is %s' %(stop-start)) # 52.763017892837524

三、gevent模块

Gevent 是一个第三方库,可以轻松实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程。

1、安装

pip3 install gevent

2、 用法介绍

g1=gevent.spawn(func,1,,2,3,x=4,y=5):# 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g2=gevent.spawn(func2)
g1.join():#等待g1结束
g2.join():#等待g2结束

#上述两步合成一步:
gevent.joinall([g1,g2])
g1.value
:#拿到func1的返回值

1、遇到io主动切换

import gevent

def eat(name):
    print('%s eat 1' %name)
    gevent.sleep(2)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)
    gevent.sleep(1)
    print('%s play 2' %name)


g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
# 或者gevent.joinall([g1,g2])
print('主')

上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了。

from gevent import monkey;monkey.patch_all()

必须放到被打补丁者的前面,如time,socket模块之前。或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头。

from gevent import monkey;monkey.patch_all()

import gevent
import time

def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print('主')

2、 查看threading.current_thread().getName()

我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程

from gevent import monkey;monkey.patch_all()
import threading
import gevent
import time

def eat():
    print(threading.current_thread().getName())
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print(threading.current_thread().getName())
    print('play 1')
    time.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print('主')

3、Gevent之同步与异步

from gevent import spawn,joinall,monkey;monkey.patch_all()

import time

def task(pid):
    """
    Some non-deterministic task
    """
    time.sleep(0.5)
    print('Task %s done' % pid)


def synchronous():  # 同步
    for i in range(10):
        task(i)

def asynchronous(): # 异步
    g_l=[spawn(task,i) for i in range(10)]
    joinall(g_l)
    print('DONE')
    
if __name__ == '__main__':
    print('Synchronous:')
    synchronous()
    print('Asynchronous:')
    asynchronous()

#  上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。
#  初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,
#  后者阻塞当前流程,并执行所有给定的greenlet任务。执行流程只会在 所有greenlet执行完后才会继续向下走。

4、Gevent之应用

通过gevent实现单线程下的socket并发

注意:from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞。

1、 服务端

from gevent import monkey;monkey.patch_all()
from socket import *
import gevent

#如果不想用money.patch_all()打补丁,可以用gevent自带的socket
# from gevent import socket
# s=socket.socket()

def server(server_ip,port):
    s=socket(AF_INET,SOCK_STREAM)
    s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
    s.bind((server_ip,port))
    s.listen(5)
    while True:
        conn,addr=s.accept()
        gevent.spawn(talk,conn,addr)

def talk(conn,addr):
    try:
        while True:
            res=conn.recv(1024)
            print('client %s:%s msg: %s' %(addr[0],addr[1],res))
            conn.send(res.upper())
    except Exception as e:
        print(e)
    finally:
        conn.close()

if __name__ == '__main__':
    server('127.0.0.1',8080)

2、多线程并发多个客户端

from threading import Thread
from socket import *
import threading

def client(server_ip,port):
    c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
    c.connect((server_ip,port))

    count=0
    while True:
        c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8'))
        msg=c.recv(1024)
        print(msg.decode('utf-8'))
        count+=1
if __name__ == '__main__':
    for i in range(500):
        t=Thread(target=client,args=('127.0.0.1',8080))
        t.start()

到此这篇关于Python协程操作模块的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。


Tags in this post...

Python 相关文章推荐
Python中的tuple元组详细介绍
Feb 02 Python
在Python的Django框架上部署ORM库的教程
Apr 20 Python
详解Python 序列化Serialize 和 反序列化Deserialize
Aug 20 Python
Django原生sql也能使用Paginator分页的示例代码
Nov 15 Python
python取余运算符知识点详解
Jun 27 Python
python实现登录密码重置简易操作代码
Aug 14 Python
PyTorch中Tensor的维度变换实现
Aug 18 Python
将keras的h5模型转换为tensorflow的pb模型操作
May 25 Python
Pytorch损失函数nn.NLLLoss2d()用法说明
Jul 07 Python
Python logging模块进行封装实现原理解析
Aug 07 Python
django 获取字段最大值,最新的记录操作
Aug 09 Python
基于Python爬取搜狐证券股票过程解析
Nov 18 Python
Pandas实现批量拆分与合并Excel的示例代码
May 30 #Python
Python实现仓库管理系统
May 30 #Python
python单向链表实例详解
May 25 #Python
利用Python实现模拟登录知乎
May 25 #Python
python双向链表实例详解
May 25 #Python
Python实现双向链表基本操作
May 25 #Python
python实现双向链表原理
May 25 #Python
You might like
编写PHP的安全策略
2006/10/09 PHP
PHP文件上传主要代码讲解
2013/09/30 PHP
33种Javascript 表格排序控件收集
2009/12/03 Javascript
再谈javascript图片预加载技术(详细演示)
2011/03/12 Javascript
js调用百度地图及调用百度地图的搜索功能
2015/09/07 Javascript
老司机带你解读jQuery插件开发流程
2016/05/16 Javascript
Jquery AJAX POST与GET之间的区别详细介绍
2016/10/17 Javascript
Bootstrap基本插件学习笔记之折叠(22)
2016/12/08 Javascript
bootstrapvalidator之API学习教程
2017/06/29 Javascript
webstorm添加vue.js支持的方法教程
2017/07/05 Javascript
vue如何进行动画的封装
2018/09/26 Javascript
详解nodejs解压版安装和配置(带有搭建前端项目脚手架)
2018/12/06 NodeJs
解决Vue.js应用回退或刷新界面时提示用户保存修改问题
2019/11/24 Javascript
vue实现虚拟列表功能的代码
2020/07/28 Javascript
wxPython之解决闪烁的问题
2018/01/15 Python
Python爬虫爬取新浪微博内容示例【基于代理IP】
2018/08/03 Python
Python元组知识点总结
2019/02/18 Python
Python 操作 ElasticSearch的完整代码
2019/08/04 Python
python定位xpath 节点位置的方法
2019/08/27 Python
Python实现socket非阻塞通讯功能示例
2019/11/06 Python
python socket通信编程实现文件上传代码实例
2019/12/14 Python
Tensorflow中tf.ConfigProto()的用法详解
2020/02/06 Python
Python3加密解密库Crypto的RSA加解密和签名/验签实现方法实例
2020/02/11 Python
基于python 将列表作为参数传入函数时的测试与理解
2020/06/05 Python
HTML5 声明兼容IE的写法
2011/05/16 HTML / CSS
美国宠物商店:Wag.com
2016/10/25 全球购物
Clarks鞋美国官网:全球领军鞋履品牌
2017/05/13 全球购物
马来西亚综合购物网站:Lazada马来西亚
2018/06/05 全球购物
StubHub新西兰:购买和出售你的门票
2019/04/22 全球购物
质检员岗位职责
2013/12/17 职场文书
乡镇消防工作实施方案
2014/03/27 职场文书
加入学生会演讲稿
2014/04/24 职场文书
感恩母亲节演讲稿
2014/05/07 职场文书
学习三严三实对照检查材料思想汇报
2014/09/22 职场文书
机关副主任个人四风问题整改措施
2014/09/26 职场文书
挂职锻炼个人总结
2015/03/05 职场文书