Python - 协程

一、简介

协程是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

需要强调的是:

# 1.python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会*交出cup执行权限,切换其它线程运行)
# 2.单线程内开户协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)

对比操作系统控制线程的切换,用户在单线程内控制协程的切换

优点如下:

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

缺点如下:

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

总结协程特点:

         1.必须在只有一个单线程里实现并发

         2.修改共享数据不需要加锁

         3.用户程序里自己保存多个控制流的上下文栈

附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield,greenlet都无法实现,就用到了gevent模块(select机制))

二、Greenlet

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

安装:pip install greenlet

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/6/3
# Software : PyCharm

from greenlet import greenlet

def eat(name):
    print('%s eat 1' % name)
    g2.switch('egon')
    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('egon')  # 可以在第一次switch时传入参数,以后都不需要

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

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/6/3
# Software : PyCharm

# 顺序执行
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))  # 15.185868501663208

print("------------------------------------------------")

# 切换
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))  # 64.25667524337769

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

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

三、Gevent模块

安装:pip install gevent

Gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet,这是以C扩展模块形式接入Python的轻量级协程。Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式的调度。

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/6/3
# Software : PyCharm

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('主')

遇到io阻塞时会自动切换任务

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

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

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/6/3
# Software : PyCharm

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('主')

四、Gevent之同步与异步

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/6/3
# Software : PyCharm

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)


if __name__ == '__main__':
    print('Synchronous:')
    synchronous()

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

五、Gevent之应用举例

#! /usr/bin/env python3
# -*- coding:utf-8 -*-

# Author   : mayi
# Blog     : http://www.cnblogs.com/mayi0312/
# Date     : 2019/6/3
# Software : PyCharm
# Note     : 协程应用:爬虫

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


def get_page(url):
    print('GET: %s' % url)
    response = requests.get(url)
    if response.status_code == 200:
        print('%d bytes received from %s' % (len(response.text), url))


start_time = time.time()
gevent.joinall([
    gevent.spawn(get_page, 'https://www.python.org/'),
    gevent.spawn(get_page, 'https://www.yahoo.com/'),
    gevent.spawn(get_page, 'https://github.com/'),
])
stop_time = time.time()
print('run time is %s' % (stop_time - start_time))

 

上一篇:Python学习笔记--gevent库的使用


下一篇:python协程的理解