这里先引用一下百度百科的定义.html
并发,在操做系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行node
里面的一个时间段内说明很是重要,这里假设这个时间段是一秒,因此本文指的并发是指服务器在一秒中处理的请求数量,即rps,那么rps高,本文就认为高并发.python
啥?这不是你认为的高并发, 出门左转。git
若是由笔者来归纳,操做系统大概作了两件事情,计算与IO,任何具体数学计算或者逻辑判断,或者业务逻辑都是计算,而网络交互,磁盘交互,人机之间的交互都是IO。github
根据笔者经验,大多数时候在IO上面。注意,这里说得是大多数,不是说绝对。golang
由于大多数时候业务本质上都是从数据库或者其余存储上读取内容,而后根据必定的逻辑,将数据返回给用户,好比大多数web内容。而大多数逻辑的交互都算不上计算量多大的逻辑,CPU的速度要远远高于内存IO,磁盘IO,网络IO, 而这些IO中网络IO最慢。web
在根据上面的笔者对操做系统的概述,当并发高到必定的程度,根据业务的不一样,好比计算密集,IO密集,或二者皆有,所以瓶颈可能出在计算上面或者IO上面,又或二者兼有。数据库
而本文解决的高并发,是指IO密集的高并发瓶颈,所以,计算密集的高并发并不在本文的讨论范围内。编程
为了使本文歧义更少,这里的IO主要指网络IO.flask
使用协程, 事件循环, 高效IO模型(好比多路复用,好比epoll), 三者缺一不可。
不少时候,笔者看过的文章都是说协程如何如何,最后告诉我一些协程库或者asyncio用来讲明协程的威力,最终我看懂了协程,却仍是不知道它为啥能高并发,这也是笔者写本文的目的。
可是一切仍是得从生成器提及,由于asyncio或者大多数协程库内部也是经过生成器实现的。
注意上面的三者缺一不可。
若是只懂其中一个,那么你懂了三分之一,以此类推,只有都会了,你才知道为啥协程能高并发。
生成器的定义很抽象,如今不懂不要紧,可是当你懂了以后回过头再看,会以为定义的没错,而且准确。下面是定义
摘自百度百科: 生成器是一次生成一个值的特殊类型函数。能够将其视为可恢复函数。
关于生成器的内容,本文着重于生成器实现了哪些功能,而不是生成器的原理及内部实现。
简单例子以下
def gen_func(): yield 1 yield 2 yield 3 if __name__ == '__main__': gen = gen_func() for i in gen: print(i) output: 1 2 3
上面的例子没有什么稀奇的不是吗?yield像一个特殊的关键字,将函数变成了一个相似于迭代器的对象,可使用for循环取值。
协程天然不会这么简单,python协程的目标是星辰大海,从上面的例之因此get不到它的野心,是由于你没有试过send, next两个函数。
首先说next
def gen_func(): yield 1 yield 2 yield 3 if __name__ == '__main__': gen = gen_func() print(next(gen)) print(next(gen)) print(next(gen)) output: 1 2 3
next的操做有点像for循环,每调用一次next,就会从中取出一个yield出来的值,其实仍是没啥特别的,感受尚未for循环好用。
不过,不知道你有没有想过,若是你只须要一个值,你next一次就能够了,而后你能够去作其余事情,等到须要的时候才回来再次next取值。
就这一部分而言,你也许知道为啥说生成器是能够暂停的了,不过,这彷佛也没什么用,那是由于你不知到时,生成器除了能够抛出值,还能将值传递进去。
接下来咱们看send的例子。
def gen_func(): a = yield 1 print("a: ", a) b = yield 2 print("b: ", b) c = yield 3 print("c: ", c) return "finish" if __name__ == '__main__': gen = gen_func() for i in range(4): if i == 0: print(gen.send(None)) else: # 由于gen生成器里面只有三个yield,那么只能循环三次。 # 第四次循环的时候,生成器会抛出StopIteration异常,而且return语句里面内容放在StopIteration异常里面 try: print(gen.send(i)) except StopIteration as e: print("e: ", e) output: 1 a: 1 2 b: 2 3 c: 3 e: finish
send有着next差很少的功能,不过send在传递一个值给生成器的同时,还能获取到生成器yield抛出的值,在上面的代码中,send分别将None,1,2,3四个值传递给了生成器,之因此第一须要传递None给生成器,是由于规定,之因此规定,由于第一次传递过去的值没有特定的变量或者说对象能接收,因此规定只能传递None, 若是你传递一个非None的值进去,会抛出一下错误
TypeError: can't send non-None value to a just-started generator
从上面的例子咱们也发现,生成器里面的变量a,b,c得到了,send函数发送未来的1, 2, 3.
若是你有事件循环或者说多路复用的经验,你也许可以隐隐察觉到微妙的感受。
这个微妙的感受是,是否能够将IO操做yield出来?由事件循环调度, 若是你能get到这个微妙的感受,那么你已经知道协程高并发的秘密了.
可是还差一点点.嗯, 还差一点点了.
下面是yield from的例子
def gen_func(): a = yield 1 print("a: ", a) b = yield 2 print("b: ", b) c = yield 3 print("c: ", c) return 4 def middle(): gen = gen_func() ret = yield from gen print("ret: ", ret) return "middle Exception" def main(): mid = middle() for i in range(4): if i == 0: print(mid.send(None)) else: try: print(mid.send(i)) except StopIteration as e: print("e: ", e) if __name__ == '__main__': main() output: 1 a: 1 2 b: 2 3 c: 3 ret: 4 e: middle Exception
从上面的代码咱们发现,main函数调用的middle函数的send,可是gen_func函数却能接收到main函数传递的值.有一种透传的感受,这就是yield from的做用, 这很关键。
而yield from最终传递出来的值是StopIteration异常,异常里面的内容是最终接收生成器(本示例是gen_func)return出来的值,因此ret得到了gen_func函数return的4.可是ret将异常里面的值取出以后会继续将接收到的异常往上抛,因此main函数里面须要使用try语句捕获异常。而gen_func抛出的异常里面的值已经被middle函数接收,因此middle函数会将抛出的异常里面的值设为自身return的值,
至今生成器的所有内容讲解完毕,若是,你get到了这些功能,那么你已经会使用生成器了。
再次强调,本小结只是说明生成器的功能,至于具体生成器内部怎么实现的,你能够去看其余文章,或者阅读源代码.
Linux平台一共有五大IO模型,每一个模型有本身的优势与肯定。根据应用场景的不一样可使用不一样的IO模型。
不过本文主要的考虑场景是高并发,因此会针对高并发的场景作出评价。
同步模型天然是效率最低的模型了,每次只能处理完一个链接才能处理下一个,若是只有一个线程的话, 若是有一个链接一直占用,那么后来者只能傻傻的等了。因此不适合高并发,不过最简单,符合惯性思惟。
不会阻塞后面的代码,可是须要不停的显式询问内核数据是否准备好,通常经过while循环,而while循环会耗费大量的CPU。因此也不适合高并发。
当前最流行,使用最普遍的高并发方案。
而多路复用又有三种实现方式, 分别是select, poll, epoll。
select,poll因为设计的问题,当处理链接过多会形成性能线性降低,而epoll是在前人的经验上作过改进的解决方案。不会有此问题。
不过select, poll并非一无可取,假设场景是链接数很少,而且每一个链接很是活跃,select,poll是要性能高于epoll的。
至于为啥,查看小结参考连接, 或者自行查询资料。
可是本文讲解的高并发但是指的链接数很是多的。
很偏门的一个IO模型,未曾碰见过使用案例。看模型也不见得比多路复用好用。
用得不是不少,理论上比多路复用更快,由于少了一次调用,可是实际使用并无比多路复用快很是多,因此为啥不使用普遍使用的多路复用。
使用最普遍多路复用epoll, 可使得IO操做更有效率。可是使用上有必定的难度。
至此,若是你理解了多路复用的IO模型,那么你了解python为何可以经过协程实现高并发的三分之二了。
IO模型参考: https://www.jianshu.com/p/486b0965c296
select,poll,epoll区别参考: http://www.javashuo.com/article/p-txmndpyl-cm.html
上面的IO模型可以解决IO的效率问题,可是实际使用起来须要一个事件循环驱动协程去处理IO。
下面引用官方的一个简单例子。
import selectors import socket # 建立一个selctor对象 # 在不一样的平台会使用不一样的IO模型,好比Linux使用epoll, windows使用select(不肯定) # 使用select调度IO sel = selectors.DefaultSelector() # 回调函数,用于接收新链接 def accept(sock, mask): conn, addr = sock.accept() # Should be ready print('accepted', conn, 'from', addr) conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, read) # 回调函数,用户读取client用户数据 def read(conn, mask): data = conn.recv(1000) # Should be ready if data: print('echoing', repr(data), 'to', conn) conn.send(data) # Hope it won't block else: print('closing', conn) sel.unregister(conn) conn.close() # 建立一个非堵塞的socket sock = socket.socket() sock.bind(('localhost', 1234)) sock.listen(100) sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) # 一个事件循环,用于IO调度 # 当IO可读或者可写的时候, 执行事件所对应的回调函数 def loop(): while True: events = sel.select() for key, mask in events: callback = key.data callback(key.fileobj, mask) if __name__ == '__main__': loop()
上面代码中loop函数对应事件循环,它要作的就是一遍一遍的等待IO,而后调用事件的回调函数.
可是做为事件循环远远不够,好比怎么中止,怎么在事件循环中加入其余逻辑.
若是就功能而言,上面的代码彷佛已经完成了高并发的影子,可是如你所见,直接使用select的编码难度比较大, 再者回调函数素来有"回调地狱"的恶名.
实际生活中的问题要复杂的多,做为一个调库狂魔,怎么可能会本身去实现这些,因此python官方实现了一个跨平台的事件循环,至于IO模型具体选择,官方会作适配处理。
不过官方实现是在Python3.5及之后了,3.5以前的版本只能使用第三方实现的高并发异步IO解决方案, 好比tornado,gevent,twisted。
至此你须要get到python高并发的必要条件了.
在本文开头,笔者就说过,python要完成高并发须要协程,事件循环,高效IO模型.而Python自带的asyncio模块已经所有完成了.尽情使用吧.
下面是有引用官方的一个例子
import asyncio # 经过async声明一个协程 async def handle_echo(reader, writer): # 将须要io的函数使用await等待, 那么此函数就会中止 # 当IO操做完成会唤醒这个协程 # 能够将await理解为yield from data = await reader.read(100) message = data.decode() addr = writer.get_extra_info('peername') print("Received %r from %r" % (message, addr)) print("Send: %r" % message) writer.write(data) await writer.drain() print("Close the client socket") writer.close() # 建立事件循环 loop = asyncio.get_event_loop() # 经过asyncio.start_server方法建立一个协程 coro = asyncio.start_server(handle_echo, '127.0.0.1', 8888, loop=loop) server = loop.run_until_complete(coro) # Serve requests until Ctrl+C is pressed print('Serving on {}'.format(server.sockets[0].getsockname())) try: loop.run_forever() except KeyboardInterrupt: pass # Close the server server.close() loop.run_until_complete(server.wait_closed()) loop.close()
总的来讲python3.5明确了什么是协程,什么是生成器,虽然原理差很少,可是这样会使得不会让生成器便可以做为生成器使用(好比迭代数据)又能够做为协程。
因此引入了async,await使得协程的语义更加明确。
asyncio官方只实现了比较底层的协议,好比TCP,UDP。因此诸如HTTP协议之类都须要借助第三方库,好比aiohttp。
虽然异步编程的生态不够同步编程的生态那么强大,可是若是又高并发的需求不妨试试,下面说一下比较成熟的异步库
异步http client/server框架
github地址: https://github.com/aio-libs/aiohttp
速度更快的类flask web框架。
github地址:
https://github.com/channelcat/sanic
快速,内嵌于asyncio事件循环的库,使用cython基于libuv实现。
官方性能测试:
nodejs的两倍,追平golang
github地址: https://github.com/MagicStack/uvloop
为了减小歧义,这里的性能测试应该只是网络IO高并发方面不是说任何方面追平golang。
Python之因此可以处理网络IO高并发,是由于借助了高效的IO模型,可以最大限度的调度IO,而后事件循环使用协程处理IO,协程遇到IO操做就将控制权抛出,那么在IO准备好以前的这段事件,事件循环就可使用其余的协程处理其余事情,而后协程在用户空间,而且是单线程的,因此不会像多线程,多进程那样频繁的上下文切换,于是可以节省大量的没必要要性能损失。
注: 不要再协程里面使用time.sleep之类的同步操做,由于协程再单线程里面,因此会使得整个线程停下来等待,也就没有协程的优点了
本文主要讲解Python为何可以处理高并发,不是为了讲解某个库怎么使用,因此使用细节请查阅官方文档或者执行。
不管什么编程语言,高性能框架,通常由事件循环 + 高性能IO模型(也许是epoll)组成。