Python网络编程
传输模型
层次模型
各层说明 | OSI七层网络模型 | TCP/IP四层网络模型 | 对应协议 |
---|---|---|---|
供操作系统或应用进行网络通信的标准接口 | 应用层 | 应用层 | Telnet、FTP、SMTP、DNS、HTTP |
将不同的编码方式转换成网络通信中采用的标准表现形式如: UTF-8) | 表示层 | 应用层 | Telnet、FTP、SMTP、DNS、HTTP |
不同PC不同进程间建立或解除连接,插入同步点 | 会话层 | 应用层 | Telnet、FTP、SMTP、DNS、HTTP |
两个主机间端对端的数据连接与传输 | 传输层 | 传输层 | TCP、UDP |
选择路由并正确的找着目标主机 | 网络层 | 网络层 | IP、ARP、RARP、ICMP |
两个相邻节点间准确的数据传输 | 数据链路层 | 数据链路层 | 网络通信硬件及接口 |
原始比特数据在物理介质上的传输 | 物理层 | 数据链路层 | 网络通信硬件及接口 |
应用层
名称 | 解释 |
---|---|
http | 超文本传输协议,使用浏览器查询web服务器之间传输的协议 |
ftp | 文件传输协议,实现网上与主机之间文件交换的协议 |
smtp | 简单邮件传输协议,定义了邮件如何在邮件服务器之间传输的协议 |
pop | 邮件协议,定义了将用户从邮件服务器下载到本地服务器的协议 |
Telent | 远程登陆协议,远程使用网上其他计算机使用的协议,以获取其他计算机上运行或存储的信息 |
DNS | 域名系统,实现域名到IP地址之间的转换 |
传输层
网络协议是指通信双方就通信如何进行所必须共同遵守的约定和通信规则的集合。在网络上通信的双方只有遵守相同的协议,才能正确地交流信息
TCP
-
通过三次握手协议建立稳定连接
-
应用数据分割成最合适的数据块发送
-
连接可靠,不易出现乱序、丢失等现象
-
连接和检验等都需要花不少时间,效率因此会降低
UDP
-
一次性发送所有数据,无需建立连接
-
服务器可以同时发送给多个客户机
-
没有检验,容易会出现信息丢失
-
消耗资源小,处理速度快
网络层
名称 | 解释 |
---|---|
IP | 因特网上的每台计算机和其它设备都规定了一种地址,叫做IP地址” |
ARP | 地址转换协议,完成IP地址到物理地址的转化 |
RARP | 反向地址转换协议,完成物理地址到IP地址的转换 |
ICMP | 控制报文协议,发送消息,并且报告数据包的传送错误 |
IPV4和IPV6
-
ipv4是第一个被广泛应用的ip,IP是TCP/IP协议族中网络层的协议,是TCP/IP协议族的核心协议
ipv4的地址长度规定为32位,分为四段,每段8位且以点隔开,并以十进制形式表示,故每段的数值范围为0~255,即最小的为0.0.0.0,最大为255.255.255.255
其中地址块127.0.0.1被保留作环回通信用,0.0.0.0为任意使用
- 适用于一般计算机网络
- A类,1~127.主.主.主 ==>子网掩码 255.0.0.0
- B类,128~191.网.主.主 ==>子网掩码 255.255.0.0
- C类, 192~223.网.网.主==>子网掩码 255.255.255.0
- 组播及科研专用
- D类, 224~239 组播
- E类, 240~254 科研
- 适用于一般计算机网络
-
ipv6的长度是ipv4的四倍,以十六进制形式表示,其被分为8段,每段以冒号隔开
由于ipv4位数使得其地址资源已被分配完,故产生了ipv6
端口
当同时多个程序使用网络时,为了保证信息发送到正确的地方,增加了端口,不同的软件使用不同的端口,虽然使用同一个网络,但是因为端口不一样,也能保证信息发送给正确的人
端口号的范围从0到65535(2^16-1),比如用于浏览网页服务的80端口,用于FTP服务的21端口等
ip地址用于区分不同的主机,而端口号是用于区分一个主机下的不同网络服务
TCP协议
建立连接
-
第一步:客户端发送SYN报文给服务器端,进入SYN_SEND状态。
-
第二步:服务器端收到SYN报文,回应一个SYN ACK报文,进入SYN_RECV状态。
-
第三步:客户端收到服务器端的SYN报文,回应一个ACK 报文,进入Established状态。
-
三步完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了
长连接与短连接
- 短链接
- 建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接
- 长链接
- 建立连接——数据传输...(保持连接)...数据传输——关闭连接
断开连接
-
第一步:先由客户端向服务器端发送一个FIN,请求关闭数据传输。
-
第二步:当服务器接收到客户端的FIN时,向客户端发送一个ACK,其中ack的值等于FIN+SEQ
-
第三步:然后服务器向客户端发送一个FIN,告诉客户端应用程序关闭。
-
第四步:当客户端收到服务器端的FIN是,回复一个ACK给服务器端。其中ack的值等于FIN+SEQ
创建套接字
服务端
客户端
对等连接套接字
数据交互
clinet.send(b"hello") # 必须为字节类型的数据
client.recv(1024) # 接收数据,要指定数据的最大长度
client.close() # 关闭连接
阻塞
当客户端没有连接,或者发送数据的时候,服务端执行 accept 和 recv 都会产生阻塞
单工: 广播,只收对方的信息,只发不收
半双工: 同一时刻只能有一个人讲
全双工:可以同时讲和收
基本IO模型
数据流概念
-
数据流(data stream)是一组有序,有起点和终点的字节的数据序列。是只能被读取一次或少数几次的点的有序序列。其包括输入流和输出流
-
数据流分为输入流(InputStream)和输出流(OutputStream)两类。输入流只能读不能写,而输出流只能写不能读。通常程序中使用输入流读出数据,输出流写入数据,就好像数据流入到程序并从程序中流出。采用数据流使程序的输入输出操作独立与相关设备
-
输入流可从键盘或文件中获得数据,输出流可向显示器、打印机或文件中传输数据
IO解释与IO交互
-
IO即 input and ouput。在unix世界里,一切皆文件。而文件是什么呢?文件就是一串二进制流,不管socket、还是FIFO、管道、终端,一切都是文件,一切都是流。在信息交换的过程中,对这些流进行数据的收发操作,简称为I/O操作(input and output)
-
往流中读出数据,系统调用read。写入数据,系统调用write。但是计算机里有这么多的流,怎么知道要操作哪个流呢?做到这个的就是文件描述符,即通常所说的fd。一个fd就是一个整数,所以对这个整数的操作,就是对这个文件(流)的操作。创建一个socket,通过系统调用会返回一个文件描述符,那么剩下对socket的操作就会转化为对这个描述符的操作
阻塞IO
在实际情况汇总,很多时候数据在一开始还没有到达,这个时候内核就要等待足够的数据到来
而在用户进程这边,整个进程会被阻塞,当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后返回结果,用户进程才解除阻塞的状,重新运行起来
非阻塞IO模型与非阻塞套接字
非阻塞IO模型
-
从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果,用户进程判断结果是一个error时,它就知道数据还没有准备好
-
于是它可以再次发送read操作,一旦内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回,非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回
非阻塞IO
利用异常处理来处理非阻塞IO产生的异常
并发与并行
并发
指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行
代码实现很简单,但是要理解过程
主要是还是用循环等思想去解决问题
并行
指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序是在不同的处理机上运行,并任一个时刻点上有多个程序在不同处理机上运行
IO多路复用
在之前的非阻塞IO模型中,通过不断的询问来查看是否有数据,这会造成资源的浪费
将查看的过程由主动的查询,变成交给复用器完成,这样能够更加节省系统资源,并且性能更好
epoll
-
非阻塞套接字与多路复用
- 非阻塞套接字需要自身遍历每个对等连接套接字,并且每次都是进行的IO操作
复用器不需要进行大量的IO操作,复用器会告诉你哪个对等连接套接字有数据过来,然后再去处理即可
- 非阻塞套接字需要自身遍历每个对等连接套接字,并且每次都是进行的IO操作
-
epoll是一个惰性事件回调,即回调过程是用户自己去调用的,操作系统只起到通知的作用
-
epoll是Linux上最好的IO多路复用器,但是也只有在Linux上有,在其他的地方没有
多任务
CPU的时间片
时间片的切换优先调度算法
在单核中,并行的基本是并发,只有多核才能真正实现并行
进程
正在运行的程序
- 每个进程都有自己的独立的地址空间,内存,数据线,以及其他用于跟踪执行的辅助数据
- 各个进程之间相互独立,互不影响
多进程
使用多进程来分担耗时任务,在另一个进程中运行耗时任务。这样主进程就不会收到影响。
当子进程执行完时,返回子进程的运行结果
import multiprocessing, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
def func1(a):
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print(a)
print("内部结束时:", time.time())
# 模拟进程外部要做的事情
print("外部开始", time.time())
p1 = multiprocessing.Process(target=func) # 实例化一个新的进程对象,子进程
p2 = multiprocessing.Process(target=func1, args=("nihao",)) # 实例化一个子进程,同时通过args传参
p1.start() # 开启进程使用帮我们分担任务
p2.start() # 开启进程
time.sleep(5) # 模拟耗时操作
print("外部结束", time.time())
# 输出:总耗时5秒
首先是 multiprocessing.Process 实例化,并且指定回调函数,参数列表
实例化之后可以直接调用运行,这就实现了多进程运行,节省运行时间
这里并行都只是Python层面的,并不是实际层面的
当总进程数多于核心数的时候,多于的没有效果
多进程由操作系统调度运行
多线程
如果把进程比做一个工厂,那么线程就是工厂里面的工人,也就是一个进程可以包含多个线程
车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存
一个线程可以被抢占(中断) 也可以临时挂起(睡眠)等
线程的调度由Python解释器调度,而进程由操作系统调度
代码实现
import threading, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
def func1(a):
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print(a)
print("内部结束时:", time.time())
print("外部开始", time.time())
t1 = threading.Thread(target=func) # 实例化一个线程对象来分担任务
t2 = threading.Thread(target=func1, args=("nihao",)) # 实例化一个子进程,同时通过args传参
t1.start()
t2.start() # 开启线程
time.sleep(5) # 模拟耗时操作
print("外部结束:", time.time())
# 输出时间还是为5秒
GIL全局解释锁
Python发明之初,还没有多核CPU的概念。为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁
GIL锁要求,任何进程中,一次只能有一个线程在执行。因此,并不能为多个线程分配多个CPU。所以Python中的线程只能实现并发,而不能实现真正的并行
多任务运行控制
等待子任务结束
join方法
进程或着线程添加 join 方法之后,会等待子任务结束,如果没有结束则会阻塞,直到子任务结束,
因此join一般都是放在程序的最后面
import threading, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
def func1(a):
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print(a)
print("内部结束时:", time.time())
print("外部开始", time.time())
t1 = threading.Thread(target=func) # 实例化一个线程对象来分担任务
t2 = threading.Thread(target=func1, args=("nihao",)) # 实例化一个子进程,同时通过args传参
t1.start()
t2.start() # 开启线程
time.sleep(5) # 模拟耗时操作
t1.join()
t2.join() # 控制主进程等待子线程结束再结束
print("外部结束:", time.time())
# 输出时间还是为5秒
终止任务
在正常情况下,主进程的结束,并不会影响子进程,但是也可以在主进程结束之后,强制终止子进程。
注意线程不能终止,只能等待结束 -
import multiprocessing, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
def func1(a):
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print(a)
print("内部结束时:", time.time())
# 模拟进程外部要做的事情
print("外部开始", time.time())
p1 = multiprocessing.Process(target=func) # 实例化一个新的进程对象,子进程
p2 = multiprocessing.Process(target=func1, args=("nihao",)) # 实例化一个子进程,同时通过args传参
p1.start() # 开启进程使用帮我们分担任务
p2.start() # 开启进程
time.sleep(5) # 模拟耗时操作
# p1.join()
# p2.join() # 控制主进程等待子进程结束再结束
p1.terminate()
p2.terminate() # 主进程关闭是强制终止子进程,其对线程无效
print("外部结束", time.time())
# 输出:总耗时5秒
任务名字
添加与更改名字
import mltiprocessing
# 在初始化的时候定义一个名字
p1 = multiprocessing.Process(name="processName")
print(p1, p1.name)
# 在操作过程中也可以直接改名字
p1.name = "NewName"
print(p1.name)
获取当前进程
- 在进程内容获取当前进程,方便查找问题
使用mltiprocessing.cuttent_process()
多任务标识
进程的pid
在Linux中,只要进程一创建,系统就会分配一个pid,在程序运行过程中,pid都不会改变
可以通过pid查看进程对资源的使用情况,也可以通过pid来控制进程的运行
import multiprocessing, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
# 模拟进程外部要做的事情
print("外部开始", time.time())
p1 = multiprocessing.Process(target=func) # 实例化一个新的进程对象,子进程
print("启动前:", p1.pid)
p1.start() # 开启进程使用帮我们分担任务
print("启动后:", p1.pid)
time.sleep(5) # 模拟耗时操作
# p1.join()
# p1.terminate()
print("外部结束", time.time())
# 输出:总耗时5秒
线程的ident
线程还是在一个进程当中,因此不会有pid
线程由python解释器调度,为了调度方便,会有ident,类似于操作系统中的pid
import threading, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
print("外部开始", time.time())
t1 = threading.Thread(target=func) # 实例化一个线程对象来分担任务
print("启动前:", t1.ident)
t1.start()
print("启动后:", t1.ident)
time.sleep(5) # 模拟耗时操作
# t1.join()
print("外部结束:", time.time())
# 输出时间还是为5秒
生存周期
进程的生命周期开始于 start,实例化之后,进程并没有启动,只有启动之后才开始生命周期
import multiprocessing, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
# 模拟进程外部要做的事情
print("外部开始", time.time())
p1 = multiprocessing.Process(target=func) # 实例化一个新的进程对象,子进程
print("启动前:", p1.is_alive())
p1.start() # 开启进程使用帮我们分担任务
print("启动后:", p1.is_alive()) # 查看是否有生命周期,返回布尔值,查看进程是否开启
time.sleep(5) # 模拟耗时操作
# p1.join()
# p1.terminate()
print("外部结束", time.time())
# 输出:总耗时5秒
守护模式
开启守护模式之后,主进程结束,子进程会自动结束
对主进程来说,运行完毕指的是主进程代码运行完毕
主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束
对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束
import multiprocessing, time
def func():
print("内部开始时:", time.time())
time.sleep(5) # 耗时模拟操作
print("内部结束时:", time.time())
# 模拟进程外部要做的事情
print("外部开始", time.time())
p1 = multiprocessing.Process(target=func, daemon=True) # 实例化一个新的进程对象,子进程,开启守护模式
p1.start() # 开启进程使用帮我们分担任务
time.sleep(5) # 模拟耗时操作
# p1.join()
# p1.terminate()
print("外部结束", time.time())
# 输出:总耗时5秒
面向对象编程
在使用多进程或者多线程时,对应模块可以直接使用,也可以继承之后,定制使用
进程相互通信
通信隔离
进程间的隔离:可以看到在不同进程中,即便声明了全局变量,也还是没有作用。
那么如何解决呢?
Manager是进程间通信的常用解决方案,通过公共空间来实现进程间的通信
import multiprocessing # 进程模块
mg = multiprocessing.Manger() # 创建公共空间,返回一个管理器,通信媒介,可以是字典或列表
def func():
dict_var.update({"a": 3, "b": 4})
dict_var = mg.dict() # 开匹配一个字典空间,返回一个代理,通过代理来操作字典子空间
print(dict_var) # 打印字典空间,空字典
# 主进程对字典空间修改
dict_var.update({"a": 1, "b": 2})
p1 = multiprocessing.Process(target=func, args= (dict_var, ))
p1.start()
p1.join()
print(dict_var)
线程的通信
线程共享
对于线程而言,它们始终在一个进程当中,因此共享同一个内存空间,因此可以访问主进程中的数据
线程资源争夺
这个时候出现资源错误,因为CPU计算是多条指令,组合运行的,所以在运行过程中如果插入其他指令,就会造成不可预见的结果
互斥锁
对于特殊资源,可以加上锁,来保护资源,确保每次操作的完整性
import threading
var = 1
# 添加互斥锁,并且拿到锁
lock = threading.Lock()
# 定义两个线程要用做的任务
def func1():
global var # 声明全局变量
for i in range(1000000):
lock.acquire() # 操作前上锁
var += i
lock.release() # 操作完后释放锁
def func2():
global var # 声明全局变量
for i in range(1000000):
lock.acquire() # 操作前上锁
var -= i
lock.release() # 操作完后释放锁
# 创建2个线程
t1 = threading.Thread(target=func1)
t2 = threading.Thread(target=func2)
t1.start()
t2.start()
t1.join()
t2.join()
print(var)
队列
概念
先进先出
实现
- 入队 put(item)
- 出队 get()
- 测试空 empty() 近似值
- 测试满 full()
- 队列长度 qsize() 近似值
- 任务结束 task_done() 线程使用
- 等待完成 join() 线程使用
import queue
q = queue.Queue() # 括号里面加数字,可以限制元素的数量
q.put(1)
q.put(12)
q.get()
q.get()
队列计数器
import queue
q = queue.Queue(3)
q.join() # 这时候不会阻塞
q.put("a") # put之后就会阻塞,因为队列内部有个计数器,每次put都会加1
q.get() # get时计数器并不会减一
q.task_done() # 调用这个方法才会减一
队列计数器
- 队列自己内部有一个计数器,当计数为0时,join不会阻塞,否则就会阻塞
生产者和消费者模型
主线程:类似于生产者,用来生成任务
线程:类似消费者,用来处理任务
import time, queue, threading
class MyThread(threading.Thread):
def __init__(self):
super().__init__()
self.daemon = True # 开启守护模式
self.queue = queue.Queue(3) # 开启队列对象,存储三个任务
self.start() # 实例化的时候直接启动线程,不需要手动启动线程
def run(self) -> None: # run方法线程自带的方法,内置方法,在线程运行时会自动调用
while True: # 不断处理任务
func, args, kwargs = self.queue.get()
func(*args, **kwargs) # 调用函数执行任务 元组不定长记得一定要拆包
self.queue.task_done() # 解决一个任务就让计数器减一,避免阻塞
# 生产者模型
def submit_tasks(self, func, args=(), kwargs={}): # func为要执行的任务,加入不定长参数使用(默认使用默认参数)
self.queue.put((func, args, kwargs)) # 提交任务
# 重写join方法
def join(self) -> None:
self.queue.join() # 查看队列计时器是否为0 任务为空 为空关闭队列
def f1():
time.sleep(2) # 任务花费两秒钟
print("任务完成")
def f2(*args, **kwargs):
time.sleep(2)
print("任务2完成", args, kwargs)
print(time.ctime()) # 打印线程开始时间
# 实例化线程对象
mt = MyThread()
# 提交任务
mt.submit_tasks(f1)
mt.submit_tasks(f2, args=("aa", "aasd"), kwargs={"a": 2, "s": 3})
# 让主线程等待子线程结束再结束
mt.join()
# 线程结束时间
print(time.ctime())
线程的重复利用
利用生产者与消费者模型,实现线程的重复利用。主进程可以看成生产者,主要产生任务线程作为消费者主要处理任务
单线程的重复使用
线程池
生产者和消费者模型
主线程:类似于生产者,用来生成任务
线程:类似消费者,用来处理任务
开启多个线程的方式
import time, queue, threading
class MyPool():
def __init__(self, n):
self.queue = queue.Queue(3) # 开启队列对象,存储三个任务
for i in range(n): # n代表我们手动要开启多少个线程使用
threading.Thread(daemon=True).start() # 自动开启线程,不需要手动开启线程
# 消费者模型
def run(self) -> None:
while True: # 不断处理任务
func, args, kwargs = self.queue.get()
func(*args, **kwargs) # 调用函数执行任务 元组不定长记得一定要拆包
self.queue.task_done() # 解决一个任务就让计数器减一,避免阻塞
# 生产者模型
def submit_tasks(self, func, args=(), kwargs={}): # func为要执行的任务,加入不定长参数使用(默认使用默认参数)
self.queue.put((func, args, kwargs)) # 提交任务
# 重写join方法
def join(self) -> None:
self.queue.join() # 查看队列计时器是否为0 任务为空 为空关闭队列
def f1():
time.sleep(2) # 任务花费两秒钟
print("任务完成")
def f2(*args, **kwargs):
time.sleep(2)
print("任务2完成", args, kwargs)
print(time.ctime()) # 打印线程开始时间
# 实例化线程对象
mt = MyPool(2)
# 提交任务
mt.submit_tasks(f1)
mt.submit_tasks(f2, args=("aa", "aasd"), kwargs={"a": 2, "s": 3})
# 让主线程等待子线程结束再结束
mt.join()
# 线程结束时间
print(time.ctime())
内置线程池
import time
from multiprocessing.pool import ThreadPool # 线程池模块
def f1():
time.sleep(2) # 任务花费两秒钟
print("任务完成")
def f2(*args, **kwargs):
time.sleep(2)
print("任务2完成", args, kwargs)
print(time.ctime()) # 打印线程开始时间
# 实例化线程对象
pool = ThreadPool(2)
# 提交任务
pool.apply_async(f1)
pool.apply_async(f2, args=("aa", "aasd"), kwds={"a": 2, "s": 3}) # 注意要改成kwds进行字典不定长传参
# 要求 在join方法前必须要关闭队列 使它不再去接受任务
pool.close()
# 让主线程等待子线程结束再结束
pool.join()
# 线程结束时间
print(time.ctime())
进程池
内置进程池
import time
from multiprocessing.pool import Pool # 进程池模块
def f1():
time.sleep(2) # 任务花费两秒钟
print("任务完成")
def f2(*args, **kwargs):
time.sleep(2)
print("任务2完成", args, kwargs)
print(time.ctime()) # 打印线程开始时间
# 实例化进程对象
pool = Pool(2)
# 提交任务
pool.apply_async(f1)
pool.apply_async(f2, args=("aa", "aasd"), kwds={"a": 2, "s": 3}) # 注意要改成kwds进行字典不定长传参
# 要求 在join方法前必须要关闭队列 使它不再去接受任务
pool.close()
# 让主进程等待子进程结束再结束
pool.join()
# 进程结束时间
print(time.ctime())
池并发服务器
import socket # 套接字模块
from multiprocessing.pool import ThreadPool # 线程池模块
from multiprocessing import cpu_count # cpu核心数模块
# 创建服务器
server = socket.socket() # 服务端套接字对象
server.bind(("127.0.0.1", 8989))
server.listen(10)
def socket_recv(conn): # 对等连接套接字
# 处理多次数据加入循环
while True:
recv_data = conn.recv(1024) # 接收数据
if recv_data: # 如果有数据就打印,并且发送给客户端
print(recv_data)
else: # 如果没有数据,就将conn套接字关闭 并结束循环
conn.colse()
break
n = cpu_count() # 获取cpu核心数
pool = ThreadPool(n) # 根据cpu核心数开启多少个线程
while True:
conn, addr = server.accept() # 创建对等连接套接字
pool.apply_async(socket_recv, args=(conn, )) # 线程池要做的任务就是数据
import socket # 套接字模块
from multiprocessing.pool import ThreadPool # 线程池模块
from multiprocessing import cpu_count,Pool # 导入获取cpu核心数模块 加入进程池模块
# 创建服务端
server = socket.socket() # 服务端套接字对象
server.bind(('127.0.0.1',8989))
server.listen(10)
# 服务端数据处理
def socket_recv(conn): # 对等连接套接字
# 处理多次数据加入循环
while True:
recv_data = conn.recv(1024) # 接收数据
if recv_data: # 如果有数据就打印 并且发送给客户端
print(recv_data)
conn.send(recv_data)
else: # 如果没有数据就将conn套接字关闭 并结束循环
conn.close()
break
# 定义一个线程要做的任务
def accpet_process(server):
tp = ThreadPool(cpu_count()*2)
while True:
conn,addr = server.accept() # 创建对等连接套接字
tp.apply_async(socket_recv,args=(conn,))
n = cpu_count() # 获取cpu核心数
pool = Pool(n) # 开启进程池 n为进程池的数目
for i in range(n):
pool.apply_async(accpet_process,args=(server,))
# 要求 在join方法前必须要关闭队列 认它不再去接收任务
pool.close()
pool.join()
协程
概念
协程是单线程下的并发,又称微线程
协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的
优点
-
协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
-
单线程内就可以实现并发的效果,最大限度地利用cpu
缺点
-
协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
-
协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
特点
-
必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
-
附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
生成器
- 通多生成器来实现对函数的反复输入和输出,这样的输入和输出同样可以用来传递数据
def func():
print(1)
yield 2
print(3)
yield 4
print(5)
a = func() # 生成器对象
for i in a: # 可以使用for循环取值
print(i)
print(next(a)) # 也可以使用next取值
"""传递数据"""
def func():
while True:
y = yield # 通过生成器对象去接收值
print(y)
a = func() # 生成器对象
next(a) # 拿取yield对象,同样也是激活yield对象使用
a.send(111) # 发送数据
生产者和消费者模型
import time
# 定义消费者模型
def consumer():
while True:
y = yield
time.sleep(1) # 一秒后处理数据
print("处理了数据", y)
# 定义生产者模型
def producer():
con = consumer() # 拿到消费者对象,并且激活使用
next(con)
for i in range(10):
time.sleep(1)
print("发送数据", i)
con.send(i)
producer() # 直接调用生产者模型
这里的生产者和消费者,既不是进程实现,也不是线程实现,而是两个生成器之间相互协同完成的。这种就称为协程
greenlet
第三方模块,需要安装
生成器实现协程非常不方便,尤其很多协程的时候,使用greenlet就要方便很多
而这个模块来自于Python的一个衍生版 Stackless Python原生的协程(常见是标准Python是CPython),将其协程单独拿出来打包成模块,因此性能要比生成器强很多
import time
from greenlet import greenlet # 导入greenlet模块
# 定义消费者模型
def consumer():
while True:
var = pro.switch() # 切换任务到生产者,进入等待,接收数据
print("consume:", var)
# 定义生产者模型
def producer():
for i in range(10):
time.sleep(1)
print("produce:", i)
con.switch(i) # 给yield传递值
print(f"{i}生产消费完成")
# 让我们的greenlet模块来做我们的协程任务
con = greenlet(consumer)
pro = greenlet(producer) # 生产者
# greenlet模块通过switch方法来切换任务,switch内五参数时会切换任务,并且处于等待接收数据的状态,有参数的情况下,会将参数作为数据发送到无参数的switch里面
con.switch()
这里并没有解决IO阻塞的问题,但是我们使用这个时间来做别的事情了,一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果
gevent
gevent并发服务器
# 第三方模块需要下载:pip install gevent
# 猴子补丁
from gevent import monkey;monkey.patch_all() # 使用gevent模块时需要加上猴子补丁
import gevent, socket # 协程模块、套接字模块
# 创建服务端
server = socket.socket() # 服务端套接字
server.bind(("127.0.0.1", 8989))
server.listen(10)
# 服务端数据处理
def socket_recv(conn): # 对等连接套接字
# 处理多次数据加入循环
while True:
recv_data = conn.recv(1024) # 接收数据
if recv_data: # 如果有数据就打印 并且发送给客户端
print(recv_data)
conn.send(recv_data)
else: # 如果没有数据就将conn套接字关闭 并结束循环
conn.close()
break
while True:
conn, addr = server.accept()
# 实例化协程对象
gevent.spawn(socket_recv, conn)
gevent封装了epoll和greenlet,在使用的时候要更加方便
同时实现了IO阻塞时的自动切换
生产者和消费者模型
# 第三方模块需要下载:pip install gevent
# 猴子补丁
from gevent import monkey;monkey.patch_all() # 使用gevent模块时需要加上猴子补丁
import gevent # 协程模块
from gevent.queue import Queue # gevent自带的队列模块
que = Queue(3)
# 生产者模型
def producer():
for i in range(20):
print("produce:", i)
que.put(i)
# 消费者模型
def consumer():
for i in range(20):
var = que.get()
print("consume:", var)
# 由gevent模块来做我们的协程任务
pro = gevent.spawn(producer, que)
con = gevent.spawn(consumer, que)
# 等待所有协程执行完毕
gevent.joinall([pro, con])
实现异步
同步: 所有的方法都是依次执行,总花费时间是所有方法运行之和
异步:与同步相对应,异步指的是让CPU暂时搁置当前请求的响应,处理下一个请求,当通过轮询或其他方式得到回调通知后,开始运行。多任务是将异步放在子任务中完成
# gevent实现同步和异步
# 猴子补丁
from gevent import monkey;monkey.patch_all() # 使用gevent模块时需要加上猴子补丁
import gevent, time # 协程模块
# 定义一个任务,同步和异步都需要做的事情
def task(i):
time.sleep(1) # 模拟阻塞
print(f"{i}task over")
# 定义一个同步的方法,模拟同步提交任务
def syn():
for i in range(10):
task(i)
# 定义一个异步的方法 模拟异步提交任务
def asy():
gl = [gevent.spawn(task, i) for i in range(10)] # 列表推导式所用时长更短,效率更高
gevent.joinall(gl)
print("开始运行")
start = time.time() # 代码开始时
# syn()
asy()
end = time.time() # 代码结束时
print(f"运行时间{start-end}")
本文来自博客园,作者:Kenny_LZK,转载请注明原文链接:https://www.cnblogs.com/liuzhongkun/p/15802295.html