高并发如何处理python_Python也能高并发

操作系统到底在干啥?

如果由笔者来概括,操作系统大概做了两件事情,计算与IO,任何具体数学计算或者逻辑判断,或者业务逻辑都是计算,而网络交互,磁盘交互,人机之间的交互都是IO。

高并发的瓶颈在哪?

大多数时候在IO上面。注意,这里说得是大多数,不是说绝对。

因为大多数时候业务本质上都是从数据库或者其他存储上读取内容,然后根据一定的逻辑,将数据返回给用户,比如大多数web内容。而大多数逻辑的交互都算不上计算量多大的逻辑,CPU的速度要远远高于内存IO,磁盘IO,网络IO, 而这些IO中网络IO最慢。

在根据上面的笔者对操作系统的概述,当并发高到一定的程度,根据业务的不同,比如计算密集,IO密集,或两者皆有,因此瓶颈可能出在计算上面或者IO上面,又或两者兼有。

而本文解决的高并发,是指IO密集的高并发瓶颈,因此,计算密集的高并发并不在本文的讨论范围内。

为了使本文歧义更少,这里的IO主要指网络IO

Python怎么处理高并发?

使用协程, 事件循环, 高效IO模型(比如多路复用,比如epoll), 三者缺一不可。

很多时候,笔者看过的文章都是说协程如何如何,最后告诉我一些协程库或者asyncio用来说明协程的威力,最终我看懂了协程,却还是不知道它为啥能高并发,这也是笔者写本文的目的。

但是一切还是得从生成器说起,因为asyncio或者大多数协程库内部也是通过生成器实现的。

注意上面的三者缺一不可。

如果只懂其中一个,那么你懂了三分之一,以此类推,只有都会了,你才知道为啥协程能高并发。

生成器

生成器的定义很抽象,现在不懂没关系,但是当你懂了之后回过头再看,会觉得定义的没错,并且准确。下面是定义

摘自百度百科: 生成器是一次生成一个值的特殊类型函数。可以将其视为可恢复函数。

关于生成器的内容,本文着重于生成器实现了哪些功能,而不是生成器的原理及内部实现。

yield

简单例子如下

defgen_func():yield 1

yield 2

yield 3

if __name__ == '__main__':

gen=gen_func()for i ingen:print(i)

output:1

2

3

上面的例子没有什么稀奇的不是吗?yield像一个特殊的关键字,将函数变成了一个类似于迭代器的对象,可以使用for循环取值。

send, next

协程自然不会这么简单,python协程的目标是星辰大海,从上面的例之所以get不到它的野心,是因为你没有试过send, next两个函数。

首先说next

defgen_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的例子。

defgen_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))exceptStopIteration as e:print("e:", e)

output:1a:1

2b:2

3c:3e: 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

下面是yield from的例子

defgen_func():

a= yield 1

print("a:", a)

b= yield 2

print("b:", b)

c= yield 3

print("c:", c)return 4

defmiddle():

gen=gen_func()

ret= yield fromgenprint("ret:", ret)return "middle Exception"

defmain():

mid=middle()for i in range(4):if i ==0:print(mid.send(None))else:try:print(mid.send(i))exceptStopIteration as e:print("e:", e)if __name__ == '__main__':

main()

output:1a:1

2b:2

3c:3ret:4e: 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到了这些功能,那么你已经会使用生成器了。

io模型

Linux平台一共有五大IO模型,每个模型有自己的优点与确定。根据应用场景的不同可以使用不同的IO模型。

不过本文主要的考虑场景是高并发,所以会针对高并发的场景做出评价。

同步IO

同步模型自然是效率最低的模型了,每次只能处理完一个连接才能处理下一个,如果只有一个线程的话, 如果有一个连接一直占用,那么后来者只能傻傻的等了。所以不适合高并发,不过最简单,符合惯性思维。

非阻塞式IO

不会阻塞后面的代码,但是需要不停的显式询问内核数据是否准备好,一般通过while循环,而while循环会耗费大量的CPU。所以也不适合高并发。

多路复用

当前最流行,使用最广泛的高并发方案。而多路复用又有三种实现方式, 分别是select, poll, epoll。

select, poll, epoll

select,poll由于设计的问题,当处理连接过多会造成性能线性下降,而epoll是在前人的经验上做过改进的解决方案。不会有此问题。

不过select, poll并不是一无是处,假设场景是连接数不多,并且每个连接非常活跃,select,poll是要性能高于epoll的。

至于为啥,查看小结参考链接, 或者自行查询资料。但是本文讲解的高并发可是指的连接数非常多的。

小结

使用最广泛多路复用epoll, 可以使得IO操作更有效率。但是使用上有一定的难度。

至此,如果你理解了多路复用的IO模型,那么你了解python为什么能够通过协程实现高并发的三分之二了。

IO模型参考:https://www.jianshu.com/p/486b0965c296

select,poll,epoll区别参考:https://www.cnblogs.com/Anker/p/3265058.html

事件循环

上面的IO模型能够解决IO的效率问题,但是实际使用起来需要一个事件循环驱动协程去处理IO。

简单实现

下面引用官方的一个简单例子。

importselectorsimportsocket#创建一个selctor对象#在不同的平台会使用不同的IO模型,比如Linux使用epoll, windows使用select(不确定)#使用select调度IO

sel =selectors.DefaultSelector()#回调函数,用于接收新连接

defaccept(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用户数据

defread(conn, mask):

data= conn.recv(1000) #Should be ready

ifdata: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可读或者可写的时候, 执行事件所对应的回调函数

defloop():whileTrue:

events=sel.select()for key, mask inevents:

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高并发的必要条件了.

asyncio

在本文开头,笔者就说过,python要完成高并发需要协程,事件循环,高效IO模型.而Python自带的asyncio模块已经全部完成了.尽情使用吧.

下面是有引用官方的一个例子

importasyncio#通过async声明一个协程

async defhandle_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()exceptKeyboardInterrupt:pass

#Close the server

server.close()

loop.run_until_complete(server.wait_closed())

loop.close()

总的来说python3.5明确了什么是协程,什么是生成器,虽然原理差不多,但是这样会使得不会让生成器即可以作为生成器使用(比如迭代数据)又可以作为协程。

所以引入了async,await使得协程的语义更加明确。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Python 中,实现 Socket 的高并发可以使用多种方式,其中 `select` 和 `selectors` 是比较常用的两种方式。 1. 使用 `select` 实现 Socket 并发 使用 `select` 实现 Socket 的高并发,可以按照以下步骤进行: 1) 创建一个 Socket,设置为非阻塞模式,如下所示: ``` import socket server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setblocking(False) ``` 2) 绑定 Socket 地址和端口,并开始监听,如下所示: ``` server_socket.bind(('localhost', 8000)) server_socket.listen(128) ``` 3) 创建一个 `select` 对象,并将监听的 Socket 加入到该对象中,如下所示: ``` import select rlist = [server_socket] wlist = [] xlist = [] while True: rs, ws, xs = select.select(rlist, wlist, xlist) for r in rs: if r is server_socket: conn, addr = server_socket.accept() conn.setblocking(False) rlist.append(conn) else: data = r.recv(1024) if data: # 处理接收到的数据 else: r.close() rlist.remove(r) ``` 4) 在 `select` 对象的 `select()` 方法中,使用 `rlist` 参数来监听读事件,使用 `wlist` 参数来监听写事件,使用 `xlist` 参数来监听异常事件。如果有 Socket 可读、可写或者出现异常,`select()` 方法就会返回对应的列表。 5) 在返回的可读列表中,如果是监听的 Server Socket,说明有新的连接请求,需要调用 `accept()` 方法接收连接,并将新的 Socket 加入到 `rlist` 列表中;如果是普通的 Socket,说明有数据到来,需要调用 `recv()` 方法接收数据,并进行相应的处理。 2. 使用 `selectors` 实现 Socket 并发 使用 `selectors` 实现 Socket 的高并发,可以按照以下步骤进行: 1) 创建一个 Socket,并将其注册到 `selectors` 对象中,如下所示: ``` import selectors import socket sel = selectors.DefaultSelector() server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.bind(('localhost', 8000)) server_socket.listen(128) server_socket.setblocking(False) sel.register(server_socket, selectors.EVENT_READ, data=None) ``` 2) 在 `selectors` 对象的 `select()` 方法中等待事件,如下所示: ``` while True: events = sel.select(timeout=None) for key, mask in events: if key.data is None: # 处理 Server Socket 的连接请求 else: # 处理普通 Socket 的数据读取和写入 ``` 3) 注册 Server Socket 的时候,将 `data` 参数设置为 `None`,在处理连接请求时,可以将 `data` 参数设置为新连接的 Socket 对象;在处理普通 Socket 的数据读取和写入时,可以通过 `key.data` 获取到该 Socket 对应的数据。 4) 在处理普通 Socket 的数据读取和写入时,可以通过 `mask` 参数来判断是读事件还是写事件,如果是读事件,就调用 `recv()` 方法接收数据,如果是写事件,就调用 `send()` 方法发送数据。 以上就是使用 `select` 和 `selectors` 实现 Socket 并发的步骤,具体实现时需要根据具体情况进行修改。 ### 回答2: 在Python中,要实现高并发的socket编程,可以使用selectors模块。 selectors模块提供了一种应对I/O多路复用的高效方式,它基于select或者epoll等系统调用,可以同时监视多个文件对象(如socket),并在有数据到来时进行处理。 首先,我们需要创建一个selectors对象,并注册要监视的socket对象。可以使用默认的selectors.DefaultSelector()来创建一个selector对象。 接下来,可以使用selectors对象的register方法来注册要监视的socket对象。register方法接受一个socket对象和一个事件类型(如selectors.EVENT_READ用于读事件),并将其添加到selector对象的监视列表中。 然后,可以使用selectors对象的select方法来等待事件的发生。select方法会一直阻塞直到有事件发生,并返回一个事件列表。 最后,可以遍历事件列表,根据事件的类型来处理不同的操作。例如,如果事件类型为selectors.EVENT_READ,则可以调用socket对象的recv方法来接收数据。 使用selectors模块能够实现高并发的socket编程,可以提高程序的性能和效率。同时,selectors模块还提供了一些其他功能,比如超时处理、取消注册等,可以根据实际需要进行使用。 总之,selectors模块是Python中实现socket并发的一种高效方式,通过使用其提供的方法和功能,可以轻松实现高并发的socket编程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值