并发编程-协程

Posted luckinlee

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程-协程相关的知识,希望对你有一定的参考价值。

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

  协程是基于单线程实现并发,即只用一个主线程(cpu只用一个),为实现并发,先认识并发本质(切换+保存状态)

  cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它

  协程本质上就是一个线程,以前线程任务的切换是由操作系统控制的,遇到I/O自动切换,现在我们用协程的目的就是较少操作系统切换的开销

知识点:

1
2
3
4
5
1.协程是基于单线程来实现并发
2.协程的三种状态(和进程相同):
    运行、阻塞、就绪
3.python的线程属于内核级别,即由操作系统控制调度(如单线程遇到IO或执行时间过长就会交出cpu执行权限
,切换其他线程运行)

#一个线程实现并发
#并发的本质:
1:遇到IO阻塞,计算密集型执行时间过长,切换
2:保持原来的状态
协程:
是单线程下的并发,又称微线程,纤程,协程是一种用户态轻量级的线程,协程的控制是由用户程序自己控制调度的

#优点:
1:协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加的轻量级
2:单线程就可以实现并发的效果,最大限度的利用CPU
3:修改共享数据不用加锁
4:一个协程遇到IO操作自动切换到其他协程
#缺点:
1:协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程开启多个线程,每个线程开启协程
2:协程是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

#多进程:操作系统控制,多个进程的多个任务切换+保持状态
#多线程:操作系统控制.多个线程的多个任务切换+保持状态
#协 程:程序控制一个线程的多个任务的切换和保持状态
#协程:微并发,处理任务不易过多
协程会调度CPU,如果协程管控的任务中,遇到阻塞,他会快速的(比操作系统快)切换到另一个任务,并且能将上一个任务挂起(保持状态),让操作系统以为CPU一直在工作(yield就是一个伪协程)

yield本身就是一种在单线程下可以保存任务运行状态的方法:

技术图片
import time
def fun1():
    for i in range(10):
        print(f"第i次")
        time.sleep(1)
        yield
def fun2():
    g = fun1()
    for k in range(10):
        next(g)             #第一次next,执行到yield结束,再次yield,继续yield下面的代码
fun1()
fun2()

#打印结果:
第0次
第1次
第2次
第3次
第4次
第5次
第6次
第7次
第8次
第9次
通过yield实现任务切换+保存装袋
技术图片
# 计算密集型:串行与协程的效率对比
import time
def task1():
    res = 1
    for i in range(1,100000):
        res += i


def task2():
    res = 1
    for i in range(1,100000):
        res -= i

start_time = time.time()
task1()
task2()
print(time.time()-start_time)


import time


def task1():
    res = 1
    for i in range(1, 100000):
        res += i
        yield res


def task2():
    g = task1()
    res = 1
    for i in range(1, 100000):
        res -= i
        next(g)


start_time = time.time()
task2()
print(time.time() - start_time)


#打印结果(可以看到:串行比协程更效率)
0.009972810745239258
0.018949270248413086
计算密集型下协程,串行的对比(串行的效率高)

yield不能检测IO

技术图片
import time
def fun1():
    while True:
        print("func1")
        yield
def fun2():
    g = fun1()
    for i in range(100000):
        next(g)
        time.sleep(3)
        print("func2")
start_time = time.time()
fun2()
print(time.time() - start_time)
示例

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

1
2
3
4
5
6
7
8
9
10
11
优点:
    1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
    2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点:
    1. 协程的本质是单线程下,无法利用多核。
    2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
特点:
    1.必须在只有一个单线程里实现并发
    2.修改共享数据不需加锁
    3.用户程序里自己保存多个控制流的上下文栈
    4.一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制)  

 

二.greenlet

要想在多个任务之间进行切换,使用greenlet可以及其简单地实现。但是,greenlet只是提供了一种比yield更简单的切换方式,当切到一个任务时遇到IO,还是原地阻塞。

技术图片
#版本一:切换 +保持状态(遇到IO不会主动切换)

greenlet是协程的底层
from greenlet import greenlet
import time
def eat(name):
    print(%s eat 1 %name)  # 2
    # g2.switch(‘taibai‘)  # 3
    time.sleep(3)
    print(%s eat 2 %name)  # 6
    g2.switch()  # 7

def play(name):
    print(%s play 1 %name)  # 4
    g1.switch()  # 5
    print(%s play 2 %name)  # 8

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

g1.switch(taibai)  # 1 切换到eat任务

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

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

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

 技术图片

上图是协程真正的意义:即一旦遇到IO操作,就切换到其他地方执行,怎么搞?为了提高效率,就用到了Geven模块

三.Gevent模块

可轻松通过gevent实现并发同步或异步编程,在gevent中用到的模式主要是Greenlet

1
2
3
4
5
6
7
8
9
g1 = gevent.spawn(func,1,3,x=1) 创建一个协程对象,func为函数名(要执行的任务),后面是参数,spawn是异步提交任务
 
 
g1.join()
g2.join()
gevent.joinall([g1,g2])  是上面2步的合体
 
 
g1.value  #得到func的返回值

遇到gevent可以识别的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,"alex")          #创建协程对象,同时提交任务
g2 = gevent.spawn(play,"egon")
gevent.joinall([g1,g2])        #没有此代码,程序一开始就完了,因为会自动切到"主协程",主协程一完,就相当于主线程完了。。。



#执行结果:
alex eat 1
egon play 1
egon play 2
alex eat 2

遇到IO就切
遇到IO切换

而如果遇到time.sleep(3)或者其他阻塞,gevent不能识别,就需要用到gevent下面的monkey模块(放在所整个py文件的最上方),此模块识别所有IO,等同于套接字.setblocking(False)

技术图片
from gevent import monkey;monkey.patch_all()
import gevent,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("__主__")



#结果:
eat food 1
play 1
play 2
eat food 2
__主__

*-------------------------------
import time

from gevent import monkey
monkey.patch_all()
import gevent
def task(name):
    print(fname is running)
    time.sleep(2)
    print(fname is gone)

def task1(name):
    print(fname is running)
    time.sleep(3)
    print(fname is gone)

if __name__ == __main__:
    g1=gevent.spawn(task,立业)
    g2=gevent.spawn(task1,哈哈)
    gevent.joinall([g1,g2])
    print()
gevent下monkey,识别所有IO阻塞

 

以上是关于并发编程-协程的主要内容,如果未能解决你的问题,请参考以下文章

Python并发编程——多线程与协程

并发编程-协程

并发编程之协程

并发编程---协程

python语法基础-并发编程-协程-长期维护

python并发编程之---协程