Python -- 协程

9.4 协程

9.4.1 协程初识

  1. 回顾串行,并行,并发

    串行:多个任务执行时,第一个任务开始执行,遇到IO阻塞就等待,IO阻塞结束之后,继续执行下个任务

    并行:多核,多个形成或者进程同时进行,4个CPU同时执行四个任务

    并发:多个任务看起来同时执行,实质是CPU在多个任务之间来回切换(遇到IO阻塞,计算密集型执行时间过长)

    并发的本质:

    1. 遇到IO阻塞,计算密集型执行时间过长,切换
    2. 保持线程或进程原来的状态
  2. 协程与多进程多线程比较

    多进程:操作系统控制多个进程的多个任务切换+保持状态

    多线程:操作系统控制多个线程的多个任务切换+保持状态

    协程:程序控制一个线程的多个任务的切换以及保持状态

    协程属于微并发,处理任务不易过多.

    协程会调度CPU,如果协程管控的任务中,遇到阻塞,他会快速的(比操作系统快)切换到另一个任务,并且能将上一个任务挂机(保持状态),让操作系统以为CPU一直在工作.

  3. 协程基础知识

    协程是单线程下的并发,又称为线程,纤程

    什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的.

    需要强调的是:

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

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

优点如下:

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

缺点如下:

1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结协程特点:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁(优点)
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

9.4.2 协程

  1. 计算密集型:串行与协程的效率对比
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(f'串行消耗时间:{time.time()-start_time}')0.01754593849182129

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(f'协程消耗时间:{time.time() - start_time}')0.023561477661132812
  1. 协程:遇到IO自动切换
from greenlet import greenlet
import time

# 不能自动切换,
# 遇到IO不切换
# 可以保持原来的状态.
def eat(name):

    print('%s eat 1' %name)  #2
    g2.switch('alex')   #3
    time.sleep(3)
    print('%s eat 2' %name) #6
    g2.switch() #7

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

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

g1.switch('太白')  # 1  第一次切换一定要传参

g2.switch('b1')

time.sleep(300)

# 还没有做到真正遇到IO切换
import gevent
import time
def eat(name):
    print('%s eat 1' %name)  # 1
    # gevent.sleep(2)
    time.sleep(3)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)  # 2
    # gevent.sleep(1)
    time.sleep(2)
    print('%s play 2' %name)


g1 = gevent.spawn(eat, 'alex')
g2 = gevent.spawn(play, name='taibai')
g1.join()
g2.join()
# 或者gevent.joinall([g1,g2])
gevent.joinall([g1,g2])
print('主')
import threading
from gevent import monkey
monkey.patch_all()  # 将你代码中的所有的IO都标识.

import gevent  # 直接导入即可
import time
def eat():
    print(f'线程1:{threading.current_thread().getName()}')
    print('eat food 1')
    time.sleep(3)  # 加上mokey就能够识别到time模块的sleep了
    print('eat food 2')

def play():
    print(f'线程2:{threading.current_thread().getName()}')
    print('play 1')
    time.sleep(1)  # 来回切换,直到一个I/O的时间结束,这里都是我们个gevent做得,不再是控制不了的操作系统了。
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play)
gevent.joinall([g1,g2])
print(f'主:{threading.current_thread().getName()}')
posted on 2019-08-03 19:04  ShenQiang  阅读(107)  评论(0编辑  收藏  举报