并发编程-协程

单线程实现并发出现的前提

  • 创建+销毁线程带来了内存开销

并发的实现原理是:切换+保存, 因此我们要实现并发,就需要为每一个任务创建一个线程,必然增加了线程创建销毁带来的开销

  • 高并发会导致线程数量太多,无法再开启新的线程,也就无法处理新的任务情况

为了解决上述问题:

即在保证并发效果的同时,避免创建线程带来的开销问题

协程就被提了出来,协程的原理:单线程来实现多任务并发

单线程实现并发

是否可行

并发:指的是多个任务同时发生,看起来像是同时都在进行

所以单线程实现并发在理论上是可行的

如何实现

我们说并发就是:切换+保存,因此只要保证在单线程中,两个任务之间能够切换执行并保存状态,就能实现单线程并发

python中的生成器就具备这样一个特点,每次调用next都会回到生成器函数中执行代码,这意味任务之间可以切换,并且是基于上一次运行的结果,生成器会自动保存执行状态!

因此,我们可以利用生成器来实现并发执行:

def task1():
    while True:
        yield
        print("task1 run")


def task2():
    g = task1()
    while True:
        next(g)
        print("task2 run")

task2()  

task1 run  # 先调用task1生成器,用next取一个值
task2 run  # 然后在打印task2
task1 run  # 循环再调用task1生成器,再用next取一个值
task2 run  # 再打印task2

虽然我们实现了单线程并发两个任务,但是这效率非常低,尤其碰到计算密集型时,耗时比串行执行多一倍多

greenlet 模块实现并发

实现yield来切换,使得代码结构非常混乱,如果多个任务切换,那更加混乱,因此有人专门对yield进行了封装,这就是greenlet模块

from greenlet import greenlet

def task1(name):
    print(f'{name}1 is run')
    g2.switch('jack')
    print(f'{name}2 is run')
    g2.switch()

def task2(name):
    print(f'{name}1 is run')
    g1.switch()
    print(f'{name}2 is run')


g1 = greenlet(task1)
g2 = greenlet(task2)

g1.switch('rose') # switch第一次执行时传参,以后就不需要再传

rose1 is run
jack1 is run
rose2 is run
jack2 is run

greenlet 模块简化了yield复杂的代码,也实现了单线程多任务的并发,但是无论yield还是greenlet都不能检测IO操作,遇到IO时同样进入阻塞状态,同样对于计算密集型任务效率也低

协程

协程:是单线程下的并发,又称为微线程,纤程。英文名:coroutine。是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的;而线程和进程是由操作系统调度的

需要注意的是:

  • python的线程属于内核级别的,即由操作系统控制调度(如果单线程遇到IO或执行超时会被迫交出CPU执行权限,切换到其他线程运行
  • 单线程内开启协程,一旦遇到IO操作,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非IO操作的切换与效率无关)

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

  • 优点:
    • 协程的切换开销小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
    • 单线程内就可以实现并发的效果,最大限度的利用CPU
  • 缺点:
    • 协程的本质是单线程内运行,因此无法利用多核CPU,(解决:可以一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程,来提高效率)
    • 协程本质是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

gevent模块

协程的实现基于gevent模块,Gevent是一个第三方库,因此需要下载,可以通过gevent实现并发编程,在gevent中用到的主要模式是greenlet,它是以C扩展模块形式接入python的轻量级协程。greenlet全部运行在主程序操作系统进程的内部,但是他们被协作式的调度。

常用方法

import gevent

def task1(name):
    print(f'{name}1 is run')



def task2(name):
    print(f'{name}2 is run')

g1 = gevent.spawn(task1,'jack') # 实例化一个协程对象,spawn括号内第一个参数是函数名,如task1,后面可以有多个参数,可以是位置实参或者关键字实参,用逗号隔开
g2 = gevent.spawn(task2,'rose')

g1.join() #等待g1结束,之所以加join是因为,主线程结束,协程任务会立即结束
g2.join()

jack1 is run # 本质上还是串行,遇到IO操作才会切换,但是需要导入monkey补丁,如果不导入补丁,还是会				串行
rose2 is run

导入monkey模块

monkey补丁原理是把原始的阻塞方法替换为修改后的非阻塞方法,以此来实现IO自动切换

注意:一定要导入monkey模块,并实现patch_all(),才能再导入其他模块

from gevent import monkey
monkey.patch_all()

import gevent,time

def task1(name):
    print(f'{name}1 is run')
    time.sleep(2)
    print(f'{name}2 is over')


def task2(name):
    print(f'{name}1 is run')
    time.sleep(1)
    print(f'{name}2 is over')

g1 = gevent.spawn(task1,'jack')
g2 = gevent.spawn(task2,'rose')

g1.join()
#g2.join() # 可以只写一个执行时间最长的join

jack1 is run
rose1 is run # 遇到IO操作会自动切换
rose2 is over
jack2 is over

注意

  • 如果主线程结束了,协程任务也会立即结束
  • monkey补丁的原理是把原始的阻塞方法替换为非阻塞方法,必须在打补丁后再执行模块的相应功能
posted @ 2019-08-02 08:41  raynduan  阅读(160)  评论(0编辑  收藏  举报