欢迎来到Cecilia陈的博客

孤独,是人一生最好的修行。

123 协程基础

一、线程、进程回顾

  1. 在操作系统中进程是资源分配的最小单位,线程是CPU调度的最小单位。

  2. 并发的本质:切换+保存状态。

  3. cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长。

  4. 在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态。

  5. 其中并发并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。

二、协程介绍

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。

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

需要强调的是

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

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

重点:遇到io切换的时候才有意义

具体: 协程概念本质是程序员抽象出来的,操作系统根本不知道协程存在,也就说来了一个线程我自己遇到io 我自己线程内部直接切到自己的别的任务上了,操作系统跟本发现不了,也就是实现了单线程下效率最高.

优点

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

缺点

  1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程,自己要检测所有的io,但凡有一个阻塞整体都跟着阻塞.
  2. 协程指的是单个线程,因而一旦协程出现一个阻塞,没有切换任务,将会阻塞整个线程

特点

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
import time
def eat():
    print('eat 1')
    # 疯狂的计算呢没有io
    time.sleep(2)
    # for i in range(100000000):
    #     i+1
def play():
    print('play 1')
    # 疯狂的计算呢没有io
    time.sleep(3)
    # for i in range(100000000):
    #     i+1
play()
eat() # 5s

在单线程里,利用yield来实现协程,这是一个没有意义的携程(因为我们说过协程要做在有io的情况下才有意义)

import time
def func1():
    while True:
        1000000+1
        yield

def func2():
    g = func1()
    for i in range(100000000):
        i+1
        next(g)

start = time.time()
func2()
stop = time.time()
print(stop - start) # 17.68560242652893

对比上面yeild切换运行的时间,反而比我们单独取执行函数串行更消耗时间,所以上面实现的携程是没有意义的。

import time

def func1():
    for i in range(100000000):
        i+1
def func2():
    for i in range(100000000):
        i+1

start = time.time()
func1()
func2()
stop = time.time()
print(stop - start) # 12.08229374885559

三、协程的本质

协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

  1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
  2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换

3.1 使用协程我们需要用到genvent模块

重点:使用gevent来实现协程是可以的,但是我们说过协程最主要是遇到IO才有意义,但是恰好这个gevent模块做不到协程的真正的意义,也就是说这个而模块他检测不到IO

但用gevent模块是检测不到IO的,也就是说这样写同样是没有意义的

下面程序里的gevent是一个类

  1. gevent.spawn本质调用了gevent.greenlet.Greenlet的类的静态方法spawn:

    @classmethod
    def spawn(cls, *args, **kwargs):
        g = cls(*args, **kwargs)
        g.start()
        return g
    
  2. 这个类方法调用了Greenlet类的两个函数,_init_ 和 start. init函数中最为关键的是这段代码: 

    def __init__(self, run=None, *args, **kwargs):
       greenlet.__init__(self, None, get_hub()) # 将新创生的greenlet实例的parent一律设置成hub
       if run is not None:
       self._run = run
    
# 在这段程序我们发现,这段程序并没有实现遇见IO的时候,用户模cpu实现任务切换
import gevent
import time

def eat():
    print('eat 1')
    time.sleep(2)
    print('eat 2')
def play():
    print('play 1')
    # 疯狂的计算呢没有io
    time.sleep(3)
    print('play 2')

start = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
end = time.time()
print(end-start) 5.0041022300720215

'''
结果:
eat 1
eat 2
play 1
play 2
5.004306077957153
'''

重点二:使用gevent的一个补丁来实现,通过gevent类来实现真正有意义的协程,用户真正的实现里以操作系统发现不了的方式,模拟了遇见IO的时候实现任务之间的来回切换

注意:这里再次强调,协程的本质意义是在单线程内实现任务的保存状态加切换,并且真正的协程必须是在遇到IO的情况

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

def eat():
    print('eat 1')
    time.sleep(2)
    print('eat 2')
def play():
    print('play 1')
    # 疯狂的计算呢没有io
    time.sleep(3)
    print('play 2')

start = time.time()
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
g1.join()
g2.join()
end = time.time()
print(end-start)# 3.003168821334839

'''
结果:
eat 1
play 1
eat 2
play 2
3.003168821334839
'''
posted @ 2019-09-22 20:41  Cecilia陈  阅读(162)  评论(0编辑  收藏  举报