Python层面中的高并发
一、传输模型
(一).基本模型
(二).层次划分
七层模型与四层模型
作为Python开发,都是在应用层的HTTP协议之上进行开发的。HTTP协议是基于TCP之上的,也就是Python开发需要关心的是传输层。
二、TCP连接
(一).建立连接(三次握手)
第一次,只是客户端告诉服务端。
第二次,客户端才知道服务端收到了。
第三次,服务端才知道客户端收到了。
(二).传输数据
客户端向服务端请求,服务端向客户端响应。
一发一收,一收一发。
(三).断开连接(四次挥手)
因为服务端可能还有数据要发给客户端,所以在断开时就多了一次挥手。
三、IP地址与端口
(一).IP
127.0.0.1(localhost):本地回环
0.0.0.0:任意IP都可以访问
(二).端口
端口是用来区分我们的应用程序的
端口的范围:0-65535,其中0-1023多为知名端口,1024-65535多为动态端口
四、套接字编程
(一).套接字基本概念
(二).三种套接字
(三).套接字编程的常用方法
绑定套接字:bind()
服务端监听套接字:listen()
客户端连接服务端套接字:connect()
对等连接套接字:accept()
发送数据套接字:send()
接收数据套接字:recv()
关闭连接套接字:close()
(四).示例
(1).服务端代码
# 服务端
import socket
from datetime import datetime
server = socket.socket() # 实例化
server.bind(("127.0.0.1", 3333)) # 绑定
server.listen(16) # 监听
print(server)
while 1:
conn, addr = server.accept() # 生成对等连接套接字
print(f"time:{datetime.now()}")
msg = conn.recv(1024) # 接收来自客户端的数据
print(f"got a message from client:{msg}")
conn.send(msg) # 把数据发送回去给客户端
conn.close() # 把对等连接套接字关了。服务不要关,不然监听不到了
(2).客户端代码
# 客户端
import socket
while 1:
client = socket.socket()
client.connect(("127.0.0.1", 3333))#连接
sentence = input("enter your message:")
msg = bytes(sentence, "utf-8")
client.send(msg)
response = client.recv(1024)
print(f"have received response from server:{response}")
client.close()
(3).先启动服务端
不启动服务器,客户端怎么连过去?比如某服务器崩了,还能访问吗?
五、非阻塞套接字
第四部分中的示例是阻塞套接字,一次只能服务一个客户端,在accept()和recv()这两处会发生阻塞。
(一).accept()阻塞
在没有新的套接字来之前,不能处理已经建立连接的套接字的请求。
(二).recv()阻塞
在没有接受到客户端请求数据之前,不能与其他客户端建立连接。
(三).普通服务器的IO模型
(四).非阻塞IO模型
六、IO多路复用
第五部分的非阻塞套接字,有着非常严重的资源浪费,CPU吃满,无用处理很多。
那么就需要考虑用更合适的技术来解决这个问题,这里使用Linux上的epoll,来解决这个问题。
epoll是目前Linux上,效率最高的IO多路复用技术!
(一).IO多路复用技术
把socket交给操作系统去监控。
(二).epoll是惰性的事件回调
惰性事件回调是由用户进程自己调用的。操作系统只起到通知的作用。
七、进程
(一).计算机执行指令示意图
(二).轮询调度实现并发执行
并发:看上去一起执行,同时在发生
并行:真正一起执行,同时在进行
调度算法:时间片轮转;优先级调度
前提:一个CPU
(三).并行需要的核心条件
并行真正的核心条件是有多个CPU
(四).多进程实现并行
(1).进程的概念
计算机程序是存储在磁盘上的可执行二进制(或其他类型)文件。
只有把它们加载到内存中,并被操作系统调用,它们才会拥有其自己的生命周期。
进程则是表示的一个正在执行的程序。
每个进程都拥有自己的地址空间、内存、数据栈,以及其他用于跟踪执行的辅助数据。
操作系统负责其上所有进程的执行。操作系统会为这些进程合理地分配执行时间。
(2).Python进程的使用流程
(3).多进程并行的必要条件
总进程数量不多于CPU核心数量!
运行的程序都是轮询调度产生的并行假象。但是在Python层面的确获得了并行!
八、线程
(一).线程的概念
线程被称作轻量级进程。与进程类似,不过它们是在同一个进程下执行的。并且它们会共享相同的上下文。
当其他线程运行时,它可以被抢占(中断)和临时挂起(也成为睡眠),也被成为:让步。
线程的轮询调度机制类似于进程的轮询调度。只不过这个调度不是由操作系统来负责,而是由Python解释器来负责。
(二).Python线程的使用流程
(三).GIL锁
全局解释器锁
Python在设计的时候,还没有多核处理器的概念。因此,为了设计方便与线程安全,直接设计了一个锁。这个锁要求,任何进程中,一次只能有一个线程在执行。
因此,并不能为多个线程分配多个CPU。所以Python中的线程只能实现并发,而不能实现真正的并行。
但是Python3中的GIL锁有一个很棒的设计,在遇到阻塞(不是耗时)的时候,会自动切换线程。因此我们可以利用这种机制来有效的避开阻塞,充分利用CPU
Django、Flask、Web2py,都是使用多线程来做。
九、并发通信
(一).进程间的通信
进程是互不干扰的独立内存空间
进程间通信的解决方案:
1.管理器负责与公共进程通信2.代理负责操作共享的空间
(二).线程间的通信
线程属于同一个进程,是共享内存空间的。会发生资源竞争的问题,需要用互斥锁来解决这个问题。
互斥锁:控制共享资源的访问
(三).线程与进程安全的队列
队列的基本概念:
一个入口,一个出口,先入先出,First input first output(FIFO)
十、进程池与线程池
(一).池的概念
主线程: 相当于生产者,只管向线程池提交任务。并不关心线程池是如何执行任务的。因此,并不关心是哪一个线程执行的这个任务。
线程池: 相当于消费者,负责接收任务,并将任务分配到一个空闲的线程中去执行。
十一、greenlet
(一).什么是greenlet
虽然CPython(标准Python)能够通过生成器来实现协程,但使用起来还并不是很方便。
与此同时,Python的一个衍生版 Stackless Python 实现了原生的协程,它更利于使用。
于是,大家开始将 Stackless 中关于协程的代码单独拿出来做成了CPython的扩展包。
这就是greenlet的由来,因此 greenlet 是底层实现了原生协程的C扩展库
(二).安装包
需要额外安装依赖包:sudo pip3 install greenlet
(三).greenlet的价值
(1).高性能的原生协程
(2).语义更加明确的显式切换
(3).直接将函数包装成协程,保持原有代码风格
十二、gevent协程
(一).什么事gevent协程
虽然,我们有了 基于 epoll 的回调式编程模式,但是却难以使用。
即使我们可以通过配合 生成器协程 进行复杂的封装,以简化编程难度。
但是仍然有一个大的问题: 封装难度大,现有代码几乎完全要重写。
gevent,通过封装了 libev(基于epoll) 和 greenlet 两个库。
帮我们做好封装,允许我们以类似于线程的方式使用协程。
以至于我们几乎不用重写原来的代码就能充分利用 epoll 和 协程 威力。
(二).安装包
sudo pip3 install gevent
(三).gevent的价值
遇到阻塞就切换到另一个协程继续执行 !
(1).使用基于epoll的libev来避开阻塞
(2).使用基于gevent的高效协程来切换执行
(3).只在遇到阻塞的时候切换,没有轮询的开销,也没有线程的开销