看到这个难点先别划走!问题不大 看看再说!
关于并发、并行、阻塞、非阻塞、协程等这些内容不会单纯的只靠文字来讲解,会通过代码示例来帮助大家辅助理解的。
本篇文章较长,可滑到自己需要的部分查看;
文章内容:
1、异步编程了解
2、阻塞/非阻塞
3、并发与并行
4、协程
5、事件驱动与异步IO
6、异步编程的六种方法
7、概念小结
01 异步编程的相关概念
以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。
如果在某程序的运行时,能根据已经执行的指令准确判断它接下来要进行哪个具体操作,那它是同步程序,反之则为异步程序。
(简单来记就是:同步有序,异步无序)
如何在Python中进行异步编程?
先看一个同步编程的示例:
deffun(length,a): b = a for i inrange(length): a+=1 print("value of a before: "+str(b)+" now it's "+str(a)) return a defmain(): r1 =fun(50000000,0) r2 =fun(100,12) r3 =fun(100,41) if __name__=="__main__": main()
输出
这段代码传递了for循环的范围,执行代码耗时长达13.843秒,因为r1的范围是5000,所以耗时久。
现在的问题是:
必须先待r1任务完成,否则无法得到r2和r3。能在得到r1之前就得到r2和r3吗?能!异步编程就可以用上了,看一下使用异步包后的代码:
import asyncio asyncdeffun(length,a): b = a for i inrange(length): a+=1 if i %10000==0: await asyncio.sleep(0.0001) print("value of a before: "+str(b)+" now it's "+str(a)) return a asyncdefmain(): #creating subroutines. t1 = loop.create_task(fun(50000000,0)) t2 = loop.create_task(fun(100,12)) t3 = loop.create_task(fun(100,41)) await asyncio.wait([t1,t2,t3]) if __name__=="__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
输出-1
输出-2
输出-1中首先能得到t2和t3进程的结果,然后在输出-2的截图中得到了t1进程的结果,这是异步编程的功劳。
t1进程耗时最长,所以它的结果最后产生,且t1、t2和t3进程均并行运行。
异步编程的好处就在于不必等待任何进程的结果,便可获得下一个进程的结果。
02 阻塞/非阻塞
这两者概念不是很好区分,从定义上来理解:
阻塞
在进行socket通信过程中,一个线程发起请求,如果当前请求没有返回结果,则进入sleep状态。
期间线程挂起不能做其他操作,直到有返回结果,或者超时(如果设置超时的话)。
非阻塞
与阻塞相似,只不过在等待请求结果时,线程并不挂起而是进行其他操作。
即在不能立刻得到结果之前,该函数不会阻挂起当前线程,而会立刻返回。
回到代码上,首先实现阻塞方式的请求函数;
def blocking_way(): sock = socket.socket() sock.connect(('www.sina.com',80)) request = 'GET / HTTP/1.0\r\nHOST:www.sina.com\r\n\r\n' sock.send(request.encode('ascii')) response = b'' chunk = sock.recv(4096) while chunk: response += chunk chunk = sock.recv(4096) return response
测试线程、多进程和多线程
# 阻塞无并发@tsfuncdef sync_way(): res = [] for i in range(10): res.append(blocking_way()) return len(res)@tsfunc# 阻塞、多进程def process_way(): worker = 10 with futures.ProcessPoolExecutor(worker) as executor: futs = {executor.submit(blocking_way) for i in range(10)} return len([fut.result() for fut in futs])# 阻塞、多线程@tsfuncdef thread_way(): worker = 10 with futures.ThreadPoolExecutor(worker) as executor: futs = {executor.submit(blocking_way) for i in range(10)} return len([fut.result() for fut in futs])
实现非阻塞的请求代码,与阻塞方式的区别在于等待请求时并不挂起而是直接返回。
为了确保能正确读取消息,最原始的方式就是循环读取,知道读取完成为跳出循环。
def nonblocking_way(): sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) sock.setblocking(False) try: sock.connect(('home.sina.com', 80)) except BlockingIOError: pass request = 'GET / HTTP/1.0\r\nHost: home.sina.com\r\n\r\n' data = request.encode('ascii') while True: try: sock.send(data) break except OSError: pass
response = b'' while True: try: chunk = sock.recv(4096) while chunk: response += chunk chunk = sock.recv(4096) break except OSError: pass
return response
如果大部分时间浪费了并没有发挥异步编程的威力,解决的办法就是后面提到的事件驱动。
03 并发与并行
并发
描述的是程序的组织结构。指程序要被设计成多个可独立执行的子任务。指的是同一时间段可执行多个任务。
Python中的并发是通过线程和异步IO模块的帮助来实现的;
Python中可用的基本并发类型:
- threading
- asyncio
- multiprocessing
并行
描述的是程序的执行状态。指多个任务同时被执行。指的是同一时刻可执行多个任务。
并发包含并行,是比并行更广泛的概念。
同一个CPU核心上多线程切换执行时并发,多个CPU核心上多进程同时执行是并行。
需要注意的是:单核多线程一定是并发,多核多线程不一定是并行。
只有当多核多线程处理任务时,这两个线程被分配到不同的核上执行时,这两个任务才是并行的。
在Python开发过程中,处理高并发和并行有三个常见又重要的解决模型方法。
- 多线程 (multithreading)
- 多进程 (multiprocessing)
- 异步编程(asynchronous programming)
并发和并行的区别就是一个人同时吃三个馒头和三个人同时吃三个馒头的区别。
04 关于协程
又称为微线程,在一个线程中执行,执行函数时可以随时中断,由程序(用户)自身控制。
执行效率极高,与多线程比较,没有切换线程的开销和多线程锁机制。
协程的主要应用场景是 IO 密集型任务,总结几个常见的使用场景:
- 网络请求,比如爬虫,大量使用 aiohttp
- 文件读取, aiofile
- web 框架, aiohttp, fastapi
- 数据库查询, asyncpg, databases
1、创建协程
在函数前面加上async关键字,这个函数对象就是一个协程通过isinstance函数,确认他是否为Coroutine类型。
from collections.abc import Coroutineimport asyncio async def hello(name): await 1 print("hello",name) # @asyncio.coroutine# def hello(name):# yield from asyncio.sleep(6) if __name__ == "__main__": coroutine = hello("World") print(isinstance(coroutine,Coroutine)) #True
生成器是协程的基础,我们是有办法将一个生成器直接变成协程使用的;
from collections.abc import Coroutine,Generatorimport asyncio # async def hello(name):# await 1# print("hello",name) @asyncio.coroutinedef hello(name): yield from asyncio.sleep(6) if __name__ == "__main__": coroutine = hello("World") print(isinstance(coroutine,Generator)) #True print(isinstance(coroutine,Coroutine)) #False
但注意一点:
在一个生成器头部用上@asyncio.coroutine装饰器就能把这个生成器标记为协程对象。
实际上它本质还是生成器,标记后它实际上可以当成协程使用了。
2、asyncio重要概念
事件循环 ——event_loop
程序开始循环,程序员会把一些函数(协程)注册带事件循环上。当满足事情发生时,调用相应的协程函数。
协程——coroutine
协程对象,指一个使用关键字async定义的一个函数,它调用不会立刻执行函数,而是会番回忆和协程对象,协程对象注册到时间循环中,由事件循环调用。
future对象
代表将来执行或者没有执行的任务的结果,他和task上没有本质的区别
任务——task
一个协程对象就是一个原生的可以挂起的函数,任务则是对协程经一部封装,其中包含任务的各种状态。
Task对象是Future的子类,他将coroutine和Future联系在一起,将coroutine封装成一个Future对象。
async/await 关键字
async定义一个协程,await用于挂起阻塞的异步调用接口。器作用类似于tield。
3、协程工作流程
一般如下:
- 定义、创建协程对象
- 将协程转为task任务
- 定义事件循环对象容器
- 将task任务扔进事件循环对象中触发
import asyncio async def hello(name): print("hello,",name) coroutine = hello("python") loop = asyncio.get_event_loop() task = loop.create_task(coroutine) loop.run_until_complete(task)
输出结果:hello, python
4、greenlet模块
可以很方便的切换,函数之间的运行;
#安装:pip3 install greenlet
from greenlet import greenlet
def eat(name):
print('%s eat 1' %name)
g2.switch('egon')
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)
g1.switch('egon')#可以在第一次switch时传入参数,以后都不需要
5、gevent模块
import gevent
from gevent import monkey;monkey.patch_all() # 使用gevent就必须调用该模块功能,才能将整个代码变为io协程
def eat(name):
print('%s eat 1' %name)
gevent.sleep(2)
print('%s eat 2' %name)
def play(name):
print('%s play 1' %name)
gevent.sleep(1)
print('%s play 2' %name)
g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')
gevent案例
- 用于单线程下 IO密集型
- 遇到IO自动切换
- gevent.spawn(函数名,参数1,参数2) 开启协程gevent
# pip3 install gevent
from gevent import monkey;monkey.patch_all()
import gevent
import time
def eat(name):
print('%s eat 1' % name)
time.sleep(3)
print('%s eat 2' % name)
def play(name):
print('%s play 1' % name)
time.sleep(5)
print('%s play 2' % name)
g1 = gevent.spawn(eat,'zok')
g2 = gevent.spawn(play,'Kun')
gevent.joinall([g1, g2])
05 论事件驱动与异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求
上面的几种方式各有千秋:
第(1)中方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。
也就是事件驱动的方式来处理,综合考虑各方面因素,目前主流的网络服务模型就是事件驱动如Nginx,python的网络模型。
-> 最简单的异步IO示例:
run_until_complete():
阻塞调用,直到协程运行结束才返回
参数是future,传入协程对象时内部会自动变为future
asyncio.sleep():
模拟IO操作,这样的休眠不会阻塞事件循环
前面加上await后会把控制权交给主事件循环,在休眠(IO操作)结束后恢复这个协程。
提示:
若在协程中需要有延时操作,应该使用 await asyncio.sleep(),而不是使用time.sleep()。
因为使用time.sleep()后会释放GIL,阻塞整个主线程,从而阻塞整个事件循环。
import asyncio async def coroutine_example(): await asyncio.sleep(1) print('zhihu ID: Zarten') coro = coroutine_example() loop = asyncio.get_event_loop()loop.run_until_complete(coro)loop.close()
上面输出:会暂停1秒,等待 asyncio.sleep(1) 返回后打印;
06 异步编程的六种方法
回调函数
异步编程最基本的方法;
假如有两个函数f1和f2,后者等待前者的执行结果。如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
f1(f2);
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
注意:每个任务只能指定一个回调函数;
事件监听
采用事件驱动模式;任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
以f1和f2为例,先为f1绑定事件:
f1.on('done', f2); function f1(){ setTimeout(function () { // f1的任务代码 f1.trigger('done'); }, 1000); }
f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。
发布/订阅
又称观察者模式;
假定存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号。
其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。
//f2向信号中心Jquery订阅done信号 jQuery.subscribe("done", f2); function f1(){ setTimeout(function () { // f1的任务代码 //发布done信号 jQuery.publish("done"); }, 1000); } //f2执行完成后,取消订阅 jQuery.unsubscribe("done", f2);
我们可以通过查看”消息中心“,了解存在多少信号,多少个订阅者,从而监听程序的运行。
Promises对象
Promises对象是CommonJs工作组提出的一种规范,目的是为异步编程提供统一接口。
它的思想是每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。
比如f1的回调函数f2,可以写成:
f1().then(f2);
function f1(){
//deferred对象就是jQuery的回调函数解决方案。
var dfd = $.Deferred();
setTimeout(function () {
// f1的任务代码
//将dtd对象的执行状态从"未完成"改为"已完成",从而触发done()方法
dfd.resolve();
}, 500);
//返回promise对象
// deferred.promise()方法。它的作用是,在原来的deferred对象上返回另一个deferred对象,
//后者只开放与改变执行状态无关的方法(比如done()方法和fail()方法),
//屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),
//从而使得执行状态不能被改变。
return dfd.promise;
}
f1().then(f2).then(f3); //指定多个回调函数
f1().then(f2).fail(f3); //指定发生错误时的回调函数
生成器函数
生成器函数 Generator/ yield
- Generator 函数是 ES6 提供的一种异步编程解决方案
- yield表达式可以暂停函数执行
- next方法用于恢复函数执行
这使得Generator函数非常适合将异步任务同步化;
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
每个yield返回的是{value:yield返回的值,done:true/false(执行状态)}
function *generatorDemo() { yield 'hello'; yield 1 + 2; return 'ok'; } var demo = generatorDemo() demo.next() // { value: 'hello', done: false } demo.next() // { value: 3, done: false } demo.next() // { value: 'ok', done: ture } demo.next() // { value: undefined, done: ture }
Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口…)
async/await函数的实现
async函数返回的是一个 Promise 对象,可以使用 then 方法添加回调函数,async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
await命令后面返回的是 Promise 对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中。
async function test(){ let newTime = await new Promise((resolve,reject)=>{//这里等待异步返回结果,再继续向下执行 let time = 3000; setTimeout(()=>{ resolve(time); },time) }) console.log(newTime+'毫秒后执行'); let content = 'test'; console.log(content); //3s后,先输出 “3000毫秒后执行”,再输出 "test" } test()
07 以上概念简单小结
① 并行是为了利用多核加速多任务完成的进度
② 并发是为了让独立的子任务都有机会被尽快执行,但不一定能加速整体进度。
③ 非阻塞是为了提高程序整体执行效率
④ 异步是高效地组织非阻塞任务的方式