123 协程基础
Posted xichenhome
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了123 协程基础相关的知识,希望对你有一定的参考价值。
一、线程、进程回顾
在操作系统中进程是资源分配的最小单位,线程是CPU调度的最小单位。
并发的本质:切换+保存状态。
cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长。
在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态。
其中并发并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。
二、协程介绍
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。
一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的,单线程下实现并发。
需要强调的是:
- python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
- 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换。
重点:遇到io切换的时候才有意义
具体: 协程概念本质是程序员抽象出来的,操作系统根本不知道协程存在,也就说来了一个线程我自己遇到io 我自己线程内部直接切到自己的别的任务上了,操作系统跟本发现不了,也就是实现了单线程下效率最高.
优点:
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点:
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程,自己要检测所有的io,但凡有一个阻塞整体都跟着阻塞.
- 协程指的是单个线程,因而一旦协程出现一个阻塞,没有切换任务,将会阻塞整个线程
特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
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的补充:可以检测io操作,在遇到io操作的情况下才发生切换
3.1 使用协程我们需要用到genvent模块
重点:使用gevent来实现协程是可以的,但是我们说过协程最主要是遇到IO才有意义,但是恰好这个gevent模块做不到协程的真正的意义,也就是说这个而模块他检测不到IO
但用gevent模块是检测不到IO的,也就是说这样写同样是没有意义的
下面程序里的gevent是一个类
gevent.spawn本质调用了gevent.greenlet.Greenlet的类的静态方法spawn:
@classmethod def spawn(cls, *args, **kwargs): g = cls(*args, **kwargs) g.start() return g
这个类方法调用了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
'''
以上是关于123 协程基础的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin 协程协程底层实现 ① ( Kotlin 协程分层架构 | 基础设施层 | 业务框架层 | 使用 Kotlin 协程基础设施层标准库 Api 实现协程 )