123 协程基础

1、线程、进程回顾

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

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

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

  4. 在介绍进程理论时,说起进程的三种执行状态,而线程才是执行单位,因此也能够将上图理解为线程的三种状态。函数

  5. 其中并发并不能提高效率,只是为了让cpu可以雨露均沾,实现看起来全部任务都被“同时”执行的效果,若是多个任务都是纯计算的,这种切换反而会下降效率。spa

2、协程介绍

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

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

须要强调的是code

  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

3、协程的本质

协程的本质就是在单线程下,由用户本身控制一个任务遇到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
'''
相关文章
相关标签/搜索