并发编程之协程

Posted plf-jack

tags:

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

协程

什么是协程

? 在单个线程下实现并发效果,在多个任务之间切换。协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,当程序中存在大量不需要CPU的操作时(IO),适用于协程。

? 官方说法:协程称为微线程,就是操作系统级别的线程。是由操作系统来控制调度的。

线程出现的问题

  1. GIL锁 导致多线程无法并行执行,只能并发执行,效率低。但是并发时我们要实现的最终目的(最好并行)

  2. 线程出现假死状态
  3. 例如tcp服务器,限制了最大线程数量1000,如果第1000个客户有一部分,没有进行任何的操作,而新任务将无法被处理,即使CPU空闲

使用协程的好处

  1. 协程有极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销

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

  3. 因为协程是一个线程执行,所以想要利用多核CPU,最简单的方法是多进程+协程,这样既充分利用多核,又充分发挥协程的高效率。

符合什么条件就能称之为协程:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 一个协程遇到IO操作自动切换到其它协程

协程的使用场景

  1. IO密集型任务,且任务数量非常多。

Python中对于协程有两个模块,greenlet和gevent。

Greenlet(greenlet的执行顺序需要我们手动控制)

第1阶段:无作为

import  greenlet

def task1():
    print("task1 start")
    time.sleep(2)
    print("task1 over")


def task2():
    print("task2 start")
    time.sleep(2)
    print("task2 over")


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

start_time = time.time()
g1.switch()         # 可以理解为:开始g1协程,执行完毕后,开始g2协程
g2.switch()         # 开始g2协程
end_time = time.time() - start_time
print(end_time)
task1 start
task1 over
task2 start
task2 over
4.000658273696899

不作为的情况下,就是串行。

第2阶段:面对IO操作,手动切换协程(可以理解为并发)

import  greenlet

def task1():
    print("task1 start")
    g2.switch()     # 下面即将面对IO操作。于是切换到g2协程
    time.sleep(2)
    print("task1 over")
    g2.switch()     # g1协程执行完毕,切换到g2协程

def task2():
    print("task2 start")
    g1.switch()     # 下面即将面对IO操作。于是切换到g1协程
    time.sleep(2)
    print("task2 over")

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

# 计算时间
start_time = time.time()
g1.switch()         # 开启g1协程.这里注意g2协程已经在g1协程中开启了。所有在外面无需再开了
end_time = time.time() - start_time

print(end_time)
task1 start
task2 start
task1 over
task2 over
4.000972509384155

第3阶段:面对计算操作,手动切换协程(可以理解为并发,但是相对串行更复杂了)

import  greenlet

def task1():
    print("task1 start")
    g2.switch()
    for i in range(50000000):       
        1 + 1
    print("task1 over")
    g2.switch()

def task2():
    print("task2 start")
    g1.switch()
    for i in range(50000000):
        1 + 1
    print("task2 over")

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

# 计算时间
start_time = time.time()
g1.switch()
end_time = time.time() - start_time

print(end_time)
task1 start
task2 start
task1 over
task2 over
2.6845099925994873

第4阶段:面对计算操作,不切换协程(可以理解为串行)

import  greenlet

def task1():
    print("task1 start")
    for i in range(50000000):
        1 + 1
    print("task1 over")


def task2():
    print("task2 start")
    for i in range(50000000):
        1 + 1
    print("task2 over")

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

# 计算时间
start_time = time.time()
g1.switch()
g2.switch()
end_time = time.time() - start_time

print(end_time)
task1 start
task2 start
task1 over
task2 over
2.6445099925994873

总结:

  1. 需要手动切换协程,增加了逻辑复杂度
  2. 效率不高,跟串行的效率相差不大

gevent(自动切换,由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成)

第1阶段:gevent对于io操作的处理方式(并发)

from gevent.monkey import  patch_all    # 猴子补丁。geven实现并发的原理,是将原本阻塞的代码变为非阻塞
import gevent
import time
patch_all()     # 对导入的模块进行打补丁

def task1():
    print("task1 start")
    time.sleep(2)       # 遇到阻塞操作,切换到另外一个任务
    print("task1 over")

def task2():
    print("task2 start")
    time.sleep(2)       # 遇到阻塞操作,切换到另外一个任务
    print("task2 over")

g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)

# 计算时间
start_time = time.time()
g1.join()       # 开启一个  就能让所有的协程进行工作
end_time = time.time() - start_time
print(end_time)
task1 start
task2 start
task1 over
task2 over
2.003842830657959

第2阶段:gevent对于计算操作的处理方式(串行)

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

def task1():
    print("task1 start")
    for i in range(50000000):   # 此时遇到的不是IO操作,而是计算,因此协程不会切换,而是直接执行完
        1 + 1        
    print("task1 over")

def task2():
    print("task2 start")
    for i in range(50000000):
        1 + 1               # 此时遇到的不是IO操作,而是计算,因此协程不会切换,而是直接执行完
    print("task2 over")


g1 = gevent.spawn(task1)
g2 = gevent.spawn(task2)

# 计算时间
start_time = time.time()
g1.join()       # 开启一个  就能让所有的协程进行工作
end_time = time.time() - start_time
print(end_time)
task1 start
task1 over
task2 start
task2 over
2.5767011642456055

总结:

  1. 对于不同任务,采取的运行方式不同
    • 对于IO密集型操作,gevent模块采用的是并发的方式。即当协程中有IO操作(阻塞状态)时,任务立即切换到另外一个任务。
    • 对于计算密集型操作,gevent模块采用的是串行的方式。计算属于运行状态,因此协程不能切换。所以阶段二是串行的
  2. 与greenlet模块,优点:
    • 第一:gevent更智能。gevent模块遇到IO操作,自动切换协程。greenlet模块则需要手动切换
    • 第二:gevent更高效。对于不同阶段的时间可以看出,不论是IO密集型还是计算密集型,gevent消耗的时间更少
    • 第三:gevent简化了操作。不需要人为手动切换协程

因此,协程使用gevent模块更好!

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

python并发编程之协程

并发编程之协程

Python并发编程之协程

并发编程之协程

并发编程之协程

Python学习:python并发编程之协程