Python协程(一) 概述

一、协程介绍

协程 ,又被称为微线程或者纤程,是一种用户态的轻量级线程,英文名Coroutine,它是实现多任务的一种方式。

其本质就是一个单线程,协程的作用就是在一个线程中人为控制代码块的执行顺序。

具体解释如下:
在一个线程中有很多函数,我们称这些函数为子程序。当一个子程序A在执行过程中可以中断执行,切换到子程序B,执行子程序B。而在适当的时候子程序B还可以切换回子程序A,去接着子程序A之前中断的地方(即回到子程序A切换到子程序B之前的状态)继续往下执行,这个过程,我们可以称之为协程。

二、Yield生成器的方式实现协程

在Python中,yield(生成器)可以很容易的实现上述的功能,从一个函数切换到另外一个函数。

由于比较繁琐,这里不再赘述,可以参考:https://blog.csdn.net/weixin_41599977/article/details/93656042

三、Greenlet模块

Greenlet是一个用C实现的协程模块,相比于python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator。

安装:

pip3 install greenlet
使用:
from greenlet import greenlet
import time

def task_1():
    while True:
        print("--This is task 1!--")
        g2.switch()  # 切换到g2中运行
        time.sleep(0.5)

def task_2():
    while True:
        print("--This is task 2!--")
        g1.switch()  # 切换到g1中运行
        time.sleep(0.5)
        
if __name__ == "__main__":
    g1 = greenlet(task_1)  # 定义greenlet对象
    g2 = greenlet(task_2)
    
    g1.switch()  # 切换到g1中运行

运行输出:

--This is task 1!--
--This is task 2!--
--This is task 1!--
--This is task 2!--
--This is task 1!--
--This is task 2!--
--This is task 1!--
--This is task 2!--

四、Gevent模块

Greenlet已经实现了协程,但是这个需要人工切换,很麻烦。

Python中还有一个能够自动切换任务的模块gevent,其原理是当一个greenlet遇到IO操作(比如网络、文件操作等)时,就自动切换到其他的greenlet,等到IO操作完成,在适当的时候切换回来继续执行。

由于IO操作比较耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程 ,就保证总有greenlet在运行,而不是等待IO。

安装:

pip3 install gevent

用法:

g1=gevent.spawn(func,1,2,3,x=4,y=5)
# 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的,spawn是异步提交任务

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束  有人测试的时候会发现,不写第二个join也能执行g2,是的,协程帮你切换执行了,但是你会发现,如果g2里面的任务执行的时间长,但是不写join的话,就不会执行完等到g2剩下的任务了

#或者上述两步合作一步:
gevent.joinall([g1,g2])

g1.value #拿到func1的返回值

遇到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])

上例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()  
    print('eat food 1')
    time.sleep(2)  # 加上monkey就能够识别到time模块的sleep了
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)  # 来回切换,直到一个I/O的时间结束,这里都是我们个gevent做得,不再是控制不了的操作系统了。
    print('play 2')

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

协程是通过自己的程序(代码)来进行切换的,只有遇到协程模块能够识别的IO操作的时候,程序才会进行任务切换,实现并发效果,如果所有程序都没有IO操作,那么就基本属于串行执行了。

五、Python3.x协程

Python3.x系列的协程有很多不同的地方,这里介绍下主要的:

1、asyncio

  • asyncio是Python3.4引进的标准库,直接内置了对IO的支持,asyncio的操作,需要在coroutine中通过yield from完成。
import asyncio


@asyncio.coroutine
def get_body(i):
    print(f'start{i}')
    yield from asyncio.sleep(1)
    print(f'end{i}')


loop = asyncio.get_event_loop()
tasks = [get_body(i) for i in range(5)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

输出结果:

start4
start0
start1
start3
start2
end4
end0
end1
end3
end2

它的效果是和Gevent一样的,遇到IO操作的时候,自动切换上下文。

不同的是,它对tasks的操作:task先把这个5个参数不同的函数全部加载进来,然后执行任务,任务执行是无序的。

@asyncio.coroutine把一个generator标记为coroutine类型,然后把这个coroutine扔到eventloop中执行

yield from 语法让我们方便的调用另一个generator。由于asyncio.sleep()也是一个coroutine,线程不会等待,直接中断执行下一个消息循环。当asyncio.sleep()返回时,线程可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。

2、async/await

在Python3.5的时候,asyncio添加了两个关键字aysncawait,让coroutine语法更简洁。
async关键字可以将一个函数修饰为协程对象,await关键字可以将需要耗时的操作挂起,一般多是IO操作。
它们是针对coroutine的新语法,只需要把@asyncio.coroutine替换为asyncyield from替换为await
import asyncio


async def get_body(i):
    print(f'start{i}')
    await asyncio.sleep(1)
    print(f'end{i}')


loop = asyncio.get_event_loop()
tasks = [get_body(i) for i in range(5)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

运行结果:

start3
start4
start1
start0
start2
end3
end4
end1
end0
end2

Python3.7以后的版本使用asyncio.run即可。此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。

import asyncio
 
async def work(x):  # 通过async关键字定义一个协程
    for _ in range(3):
        print('Work {} is running ..'.format(x))

coroutine_1 = work(1)  # 协程是一个对象,不能直接运行

# 方式一:
loop = asyncio.get_event_loop()  # 创建一个事件循环
result = loop.run_until_complete(coroutine_1)  # 将协程对象加入到事件循环中,并执行
print(result)  # 协程对象并没有返回结果,打印None
# 方式二:
# asyncio.run(coroutine_1)  #创建一个新的事件循环,并以coroutine_1为程序的主入口,执行完毕后关闭事件循环

 

使用asyncio实现的协程的一些特性:

  • 使用async修饰返回的协程对象,不能直接执行,需要添加到事件循环event_loop中执行。
  • 协程主要是用于实现并发操作,其本质在同一时刻,只能执行一个任务,并非多个任务同时展开。
  • 协程中被挂起的操作,一般都是异步操作,否则使用协程没有啥意义,不能提高执行效率。
  • 协程是在一个单线程中实现的,其并发并未涉及到多线程。

六、为什么要使用协程

我们广泛使用的Python解释器是CPython,而CPython解释器中存在GIL锁,它的作用就是防止多线程时线程抢占资源,所以在同一时间只允许一个线程在执行,即使在多核CPU情况下也是一样 ,所以CPU的单核和多核对于多线程的运行效率并没有多大帮助,还要在线程之间的不停切换。

基于以上情况,在一些多线程的场景时,我们就可以使用协程来代替多线程,并且可以做的更灵活。我们下面来看下协程的优势:
1、线程是系统调度的,协程是程序员人为调度的,更加灵活,简化编程模型
2、与多线程相比,协程无需上下文切换的开销,避免了无意义的调度,提高了性能
3、与多线程相比,协程不需要像线程一样,无需原子操作锁定和同步的开销

所以,在处理一些高并发场景时,有时协程比多线程更加适合,比如做爬虫时。

 

 

参考文章:

https://www.jianshu.com/p/334388949ac9

https://blog.csdn.net/weixin_41599977/article/details/93656042

https://blog.csdn.net/weixin_44251004/article/details/86594117

https://www.cnblogs.com/cheyunhua/p/11017057.html

https://www.cnblogs.com/russellyoung/p/python-zhi-xie-cheng.html

https://www.cnblogs.com/dbf-/p/11143349.html

 

 

 

posted on 2020-08-14 17:12  麦克煎蛋  阅读(1077)  评论(0编辑  收藏  举报