(一)基于单线程来实现并发
(0)并发的本质
-
基于单线程实现并发
- 即只用一个主线程(可利用的cpu只有一个)的情况下实现并发
- 并发的本质:
- 切换+保存状态
-
cpu正在运行一个任务
- 会在两种情况下去执行其他的任务
- 一:发生了IO阻塞
- 二:该任务的计算事件过长或者有一个优先级更高的任务代替了它
进程中的三种执行状态,也可以理解为线程的三种状态
- 会在两种情况下去执行其他的任务
(1)yield关键字
-
第二种情况
- 任务执行的时间过长或有优先级更改的程序出现代替了它
-
这种情况并不能提高效率
-
只是为了让cpu能够雨露均沾
-
如果多个任务都是纯计算的,那么这种切换反而会降低效率
-
yield关键字 本来就是一种在单线程下可以保存任务运行状态的方法
- yield可以保存状态
- yield的状态保存与操作系统的保存线程状态很像,到那时yield是代码级别控制的,更轻量级
- send可以把一个函数的结果传给另一个函数
- 以此来实现单线程内的程序之间的切换
- yield可以保存状态
-
单纯的切换反而会降低效率
(1)串行执行
import time
def func1():
for i in range(10000000):
i + 1
def func2():
for i in range(10000000):
i + 1
start = time.time()
func1()
func2()
stop = time.time()
print(stop - start)
(2)基于yield并发执行
import time
def func1():
while True:
yield
def func2():
g = func1()
for i in range(10000000):
i+1
next(g)
start=time.time()
func2()
stop=time.time()
print(stop-start)
(2)实现遇到IO自动切换
- 第一张情况遇到IO切换
- 任务一遇到IO,切换到任务二去执行,这样就可以利用任务一阻塞的时间完成任务二的计算
- yield不能检测IO
- 实现遇到IO自动切换
import time
def func1():
while True:
print('func1')
yield
def func2():
g = func1()
for i in range(5):
i + 1
next(g)
time.sleep(3)
print('func2')
start = time.time()
func2()
stop = time.time()
print(stop - start)
- 对于单线程下,我们不可避免程序中出现IO操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的IO操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,IO比较少,从而更多的将cpu的执行权限分配给我们的线程。
- 协程的本质就是在单线程下:
- 由用户自己控制一个任务遇到IO阻塞了就切换到另外一个任务去执行,以此来提升效率
- 为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
- 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。
- 作为1的补充:可以检测IO操作,在遇到IO操作的情况下才发生切换
(二)协程介绍
(1)什么是协程
-
协程:就是单线程下的并发,又称微线程,纤程。英文名Coroutine
-
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的
-
需要强调的是:
- python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
- 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
(2)优点
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
- 单线程内就能实现并发的效果,最大限度的利用CPU
- 应用程序级别速度要远远高于操作系统的切换速度
(3)缺点
- 协程的本质是单线程下,无法利用多核优势,可以是一个程序开启多个进程,每个进程开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程(多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地,该线程内的其他得任务都不能执行)
(4)总结
- 必须在一个线程内实现并发
- 修改共享数据不需要加锁
- 用户程序里自己保存多个控制流得上下文栈
- 一个协程遇到IO操作自动切换到其他协程(如何实现检测IO,yield、greenlet都无法实现,就用到了Gevent模块(select机制)
(三)Greenlet模块
- 如果单线程内有20个任务
- 要想实现多个任务之间切换
- 使用yield过于麻烦(首先得到生成器,然后调用next)
- greenlet模块可以非常简单得实现二十个任务得之间切换
(1)安装
- pip install greenlet
(2)使用
"""greenlet"""
# 导入模块
from greenlet import greenlet
def eat(name):
print('%s eat 1' % name)
g2.switch('uuuuu')
print('%s eat 2' % name)
g2.switch()
def play(name):
print('%s play 1' % name)
g1.switch()
print('%s play 2' % name)
# 创建对象
g1 = greenlet(eat)
g2 = greenlet(play)
# 可以在第一次switch时传入参数,以后都不需要
g1.switch('sssss')
# sssss eat 1
# uuuuu play 1
# sssss eat 2
# uuuuu play 2
- 单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度
(3)小结
- greenlet只是提高了一种相对于yield更加快捷得切换方式
- 当切换遇到一个任务时,如果遇到IO
- 那就原地阻塞,仍没有解决遇到IO切换提高效率得问题
- 单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作
- 我们完全可以在执行任务1时遇到阻塞
- 就利用阻塞的时间去执行任务2。。。。
- 如此,才能提高效率,这就用到了Gevent模块。
(四)Gevent模块
- Gevent 是一个第三方模块
- 可以轻松得通过Gevent实现并发同步或异步编程
- 在gevent中用到的主要模式是Gevent
- 它是以C扩展模块形式接入Python得轻量级协程
- Greenlet全部运行在主程序操作系统进程得内部,但他们被协作式地调度
(1)安装
- pip install gevent
(2)使用
"""gevent"""
# 导入模块
import gevent
def func(*args, **kwargs):
print(args) # (1, 2, 3)
print(kwargs) # {'x': 4, 'y': 5}
return 'ok'
def func2():
...
# 创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g1 = gevent.spawn(func, 1, 2, 3, x=4, y=5)
# 创建一个协程对象g2
g2 = gevent.spawn(func2)
g1.join() # 等待g1结束
g2.join() # 等待g2结束
(3)遇到IO自动切换任务
gevent.sleep(2)模拟的是gevent可以识别的io阻塞
# 导入模块
import gevent
def func1(name):
print("哈哈")
gevent.sleep(2)
print("哈哈1")
def func2(name):
print("嘿嘿")
gevent.sleep(2)
print("嘿嘿1")
## 创建一个协程对象g1,spawn括号内第一个参数是函数名,func1,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数func1的
f1=gevent.spawn(func1,'ss')
f2=gevent.spawn(func2,'uu')
f1.join()
f2.join()
print("主线程")
# 哈哈
# 嘿嘿
# 哈哈1
# 嘿嘿1
# 主线程
(4)其他IO
- 而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
- from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面
- 如time,socket模块之前
- 或者我们干脆记忆成:要用gevent,需要将from gevent import monkey; monkey.patch_all()放到文件的开头
import gevent
from gevent import monkey;monkey.patch_all()
def func1(name):
print("哈哈")
time.sleep(2)
print("哈哈1")
def func2(name):
print("嘿嘿")
time.sleep(2)
print("嘿嘿1")
# 创建gevent对象
f1=gevent.spawn(func1,'ss')
f2=gevent.spawn(func2,'uu')
f1.join()
f2.join()
print("主线程")
# 哈哈
# 嘿嘿
# 哈哈1
# 嘿嘿1
# 主线程
(5)gevent之同步与异步
from gevent import spawn, joinall, monkey
monkey.patch_all()
import time
def timer(func):
def inner(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
print(f'当前程序 {func.__name__} 总耗时 :>>>> {time.time() - start} s')
return res
return inner
def task(pid):
"""
Some non-deterministic task
"""
time.sleep(0.5)
print('Task %s done' % pid)
@timer
def synchronous():
for i in range(10):
task(i)
@timer
def asynchronous():
g_l = [spawn(task, i) for i in range(10)]
joinall(g_l)
if __name__ == '__main__':
print('Synchronous:')
synchronous()
print('Asynchronous:')
asynchronous()
# Synchronous:
# Task 0 done
# Task 1 done
# Task 2 done
# Task 3 done
# Task 4 done
# Task 5 done
# Task 6 done
# Task 7 done
# Task 8 done
# Task 9 done
# 当前程序 synchronous 总耗时 :>>>> 5.034381151199341 s
# Asynchronous:
# Task 0 done
# Task 1 done
# Task 2 done
# Task 3 done
# Task 4 done
# Task 5 done
# Task 6 done
# Task 7 done
# Task 8 done
# Task 9 done
# 当前程序 asynchronous 总耗时 :>>>> 0.504889726638794 s
# 上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。
# 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行所有给定的greenlet。
# 执行流程只会在 所有greenlet执行完后才会继续向下走。
标签:Task,协程,理论,gevent,done,time,print
From: https://www.cnblogs.com/suyihang/p/17987136