并发
并发编程是指在一台处理器上“同时”(同一个时间段)处理多个任务。 并发是在同一实体上的多个事件。 多个事件在同一时间间隔发生。
比如,多个线程任务在同一个CPU上快速地轮换执行,由于切换的速度非常快,给人的感觉就是这些线程任务是在同时进行的,但其实并发只是一种逻辑上的同时进行。
举个例子,你有一台单CPU的电脑,在上面打开了一个视频播放软件,这样就启动了一个进程,但是这个进程内部有2个线程,一个用于显示视频,另一个用于播放声音。在任意一个时刻,只有一个线程占用CPU,所以视频于音频并不是同时播放的,只是因为线程切换速度快到无法察觉,从宏观上看起来是“同时”的。
协程
协程,又称微线程,英文名Coroutine
。实际上,协程是运行在单线程中的“并发”,程序员通过高超的代码能力,在代码执行流程中人为的实现多任务并发,是单个线程内的任务调度技巧。协程相比多线程的一大优势就是省去了多线程之间的切换开销,获得了更高的运行效率。
特点:
- 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
- 协程切换需要的Stack远小于线程,因此可以在相同的内存中开启更多的协程。
- 由于在同一个线程上,因此可以避免出现竞争和死锁。
- 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。
python协程
yield
python中协程的原始实现方式是通过yield关键字实现,而yield关键字在python中用于实现生成器。
yield的语法规则是这样的:代码执行时遇到yield则停止执行,返回yield右侧的值(没有则返回None),遇到next(或者send)命令则从yield处继续执行,并将send命令的参数传给yield的左值变量。
看个例子:
def example_coroutine():
print('-> 启动协程')
y = 10
x = yield y
print('-> 协程接收到了x的值:', x)
ex_coro = example_coroutine()
res = next(ex_coro) # 或者ex_coro.send(None)
print(res)
ex_coro.send(10)
## 控制台输出:
-> 启动协程
10
-> 协程接收到了x的值: 10
Traceback (most recent call last):
File "d:/exercise/algorithm_exercise/py_coroutine.py", line 15, in <module>
ex_coro.send(10)
StopIteration
过程是这样的:next唤醒生成器,生成器返回变量y的值10,res接收到生成器的返回值,send(10)将10传入生成器并赋值给变量x,最后生成器结束,抛出StopIteration
异常。
实际上到这里,已经实现了一个简易的协程,通过yield和send实现任务切换。
下面的例子表示如何通过协程实现主任务与副任务切换:
import time
def task1():
while True:
count = yield "<甲>累了,让<乙>工作一会儿"
if count:
print("<甲>开始工作.....")
time.sleep(1)
else:
break
def task2(t):
# next(t)
a = t.send(None) # a = "<甲>累了,让<乙>工作一会儿"
count = 2
while count:
print("-----------------------------------")
print("<乙>开始工作.....")
time.sleep(2)
print("<乙>累了,让<甲>工作一会儿....")
ret = t.send(count)
print(ret)
count-=1
t.close()
if __name__ == '__main__':
t = task1()
task2(t)
# 控制台输出
-----------------------------------
<乙>开始工作.....
<乙>累了,让<甲>工作一会儿....
<甲>开始工作.....
<甲>累了,让<乙>工作一会儿
-----------------------------------
<乙>开始工作.....
<乙>累了,让<甲>工作一会儿....
<甲>开始工作.....
<甲>累了,让<乙>工作一会儿
asyncio模块
asyncio
是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio是一个基于事件循环的异步IO模块。
yield from
可以返回另一个生成器生成的值。yield详细介绍点这里。
asyncio可以分成三个过程:
- 创建事件循环
- 指定循环模式并运行
- 关闭循环
要注意的是,简单地调用一个协程并不会使其被调度执行。
通常我们使用asyncio.get_event_loop()
方法创建一个循环。
循环创建后可以通过run_until_complete()
方法或者run_forever()
方法来运行循环
import asyncio
import threading
@asyncio.coroutine # 声明为协程函数
def hello1():
print('Hello 1 start! (%s)' % threading.currentThread())
yield from asyncio.sleep(1) # 遇到IO,出让线程
print('Hello 1 end! (%s)' % threading.currentThread())
@asyncio.coroutine
def hello2():
print('Hello 2 start! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello 2 end! (%s)' % threading.currentThread())
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [hello1(), hello2()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
# 控制台输出
Hello 2 start! (<_MainThread(MainThread, started 21500)>)
Hello 1 start! (<_MainThread(MainThread, started 21500)>)
# 在这里阻塞1s
Hello 2 end! (<_MainThread(MainThread, started 21500)>)
Hello 1 end! (<_MainThread(MainThread, started 21500)>)
上面一段代码,使用asyncio模拟了同一个线程中的两个协程。不像上面的例子,分为主副协程,这个例子中的hello1和hello2属于平等,并不是由某个协程直接唤醒另外一个协程。虽然hello1再task列表中排在第一位,先执行的确是hello2 协程。
具体过程为@asyncio.coroutine
装饰器声明协程,调用协程并不会使其被执行。asyncio.sleep(1)
模拟1s的IO操作。hello2
协程先执行,遇到IO操作时出让CPU,然后hello1
协程被调度,同样遇到IO出让CPU,hello2的asyncio.sleep(1)
结束后,由yield from
返回hello2协程,获取CPU资源。因此虽然模拟了2s的IO操作,但是2次IO同时进行,程序总耗时大约1s。
async、await
Python3.5中对协程提供了更直接的支持,引入了async/await
关键字。使用async
代替@asyncio.coroutine
,使用await
代替yield from
,代码变得更加简洁可读。从Python设计的角度来说,async/await
让协程独立于生成器而存在,不需要再借助yield语法。async
与await
必须成双成对使用。
下面使用async和await重写上面的代码:
import asyncio
import threading
async def hello3():
print('Hello 3 start! (%s)' % threading.currentThread())
await asyncio.sleep(1)
print('Hello 3 end! (%s)' % threading.currentThread())
async def hello4():
print('Hello 4 start! (%s)' % threading.currentThread())
await asyncio.sleep(1)
print('Hello 4 end! (%s)' % threading.currentThread())
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [hello3(), hello4()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
# 控制台输出
Hello 3 start! (<_MainThread(MainThread, started 15288)>)
Hello 4 start! (<_MainThread(MainThread, started 15288)>)
Hello 3 end! (<_MainThread(MainThread, started 15288)>)
Hello 4 end! (<_MainThread(MainThread, started 15288)>)
怎么用协程
开头介绍了一些协程与线程的区别以及协程的优点,但是事实上大部分情况下,多线程都能很好的完成任务,而并不是花心思写协程。
目前,主流的代码结构设计理念需要先对要解决的问题进行抽象、分解、建模,强调的是高内聚低耦合的模块化思想。模块之间不应该相互影响,这对于互相基本不影响的多线程来说是很自然的,对于底层资源的竞争都交给操作系统来解决,上层的代码层面就可以做到模块化。而对于协程,却并不是这样,协程把资源的分配提到用户态,由程序员在代码中实现逻辑流,不可避免地会使得一些代码相互耦合。
但是多线程也不是万能的,抢断式线程调度下,操作系统对于不同线程的时间片分配使公平的,公平有时候就会导致效率低下。另外,涉及到线程非独立的时候会遇到更多的问题,比如A线程需要B线程产生的资源,那么操作系统即使切换到A线程,也会因为资源不足而必须再切回去;又比如多线程操作同一个资源,需要进行线程安全处理。这些情况下,主动出让式的协程效率则高得多。
在如今的python程序中,协程绝大多数情况下用来处理网络IO或者文件IO,但是很多情况下也是可以从协程的角度去设计代码的,很多问题从协程角度去思考可能会别有一番天地。