文章目录
- 并发编程
- 一、概述
- 二、进程
- 2.1 进程理论基础
- 2.2 基于fork的多进程编程
- fork使用
- 进程相关函数
- 2.3 孤儿和僵尸
- 2.4 multiprocessing模块创建进程
- 进程创建方法
- 进程池实现
- 2.5 进程间通信(IPC)
- 管道通信(Pipe)
- 消息队列
- 共享内存
- 信号量(信号灯集)
- 套接字:
- 三、线程编程(Thread)
- 3.1 线程基本概念
- 3.2 threading模块创建线程
- 3.3 线程对象属性
- 3.4 自定义线程类
- 3.5 同步互斥
- 线程间通信方法
- 3.6 线程同步互斥方法
- 线程Event
- 线程锁 Lock
- 死锁及其处理
- 3.7 python线程GIL
- python线程的GIL问题 (全局解释器锁)
- 四、进程VS线程
- 区别
- 联系
- 使用场景
- 要求
- 五、IO并发
- 5.1 IO 分类
- 阻塞IO
- 非阻塞IO
- 5.2 IO多路复用
- select 方法编程实现
- 扩展: 位运算
- poll方法
- epoll方法
- 5.3 协程技术
- 基础概念
- 扩展延伸@标准库协程的实现
- 第三方协程模块
- 六、并发网络通信模型
- 6.1 常见模型分类
- 6.2 基于fork的多进程网络并发模型
- 6.3 基于threading的多线程网络并发
- 6.4 ftp 文件服务器
- 6.5 HTTPServer v2.0
并发编程
一、概述
- 定义:所谓并发编程是指在一台处理器上“同时”处理多个任务。也称为多任务编程。
- 意义: 充分利用计算机多核资源,提高程序的运行效率。
- 实现方案 :多进程 , 多线程
- 并行与并发
- 并发 : 一个内核通过在多任务间来回切换实现多任务执行效果的过程。
- 类比:一个人同时做几件事,同时下载电影和听音乐(点击下载后,转而去听音乐,下载完后再看电影)
- 能够提升效率,执行IO密集型程序时提升明显,执行计算密集型程序时提升不明显。
- 一个内核同时处理多个任务,内核在任务间不断的切换。达到好像多个任务被同时执行的效果,实际每个时刻只有一个任务占有内核。
- 并行 : 多个任务利用计算机多核资源在同时执行,此时多个任务间为并行关系。(多核同时执行多个任务)
二、进程
2.1 进程理论基础
- 定义 :程序在计算机中的一次运行。
- 程序是一个可执行的文件,是静态的占有磁盘。
- 进程是一个动态的过程描述,占有计算机运行资源,有一定的生命周期。(人不可能两次踏入同一条河)
- 系统中如何产生一个进程
【1】 用户空间通过调用程序接口或者命令发起请求
【2】 操作系统接收用户请求,开始创建进程
【3】 操作系统调配计算机资源,确定进程状态等
【4】 操作系统将创建的进程提供给用户使用
- 进程基本概念
- cpu时间片:如果一个进程占有cpu内核则称这个进程在cpu时间片上。
- PCB(进程控制块 process control block):在内存中开辟的一块空间,用于存放进程的基本信息,也用于系统查找识别进程。
- 进程ID(PID): 系统为每个进程分配的一个大于0的整数,作为进程ID。每个进程ID不重复。
Linux查看进程ID : ps -aux
- 父子进程 : 系统中每一个进程(除了系统初始化进程)都有唯一的父进程,可以有0个或多个子进程。父子进程关系便于进程管理。
查看进程树: pstree
- 进程状态
- 三态
就绪态 : 进程具备执行条件,等待分配cpu资源
运行态 : 进程占有cpu时间片正在运行
阻塞态 : 进程暂时停止运行,让出cpu
- 五态 (在三态基础上增加新建和终止)
新建 : 创建一个进程,获取资源的过程
终止 : 进程结束,释放资源的过程
在这里插入图片描述
- 进程状态查看命令 : ps -aux --> STAT列
- 说明:S 等待态 R 执行态 D 等待态 T 等待态 Z 僵尸
- < 有较高优先级
- N 优先级较低
- +前台进程
- s 会话组组长
- l 有多线程的
- 前台进程既是在终端显示的进程
- 进程的结构:进程由程序、进程控制块和数据三部分组成
- 进程的特点
- 进程可以使用计算机多核资源
- 进程是计算机分配资源的最小单位
- 动态性:进程是程序的一次执行过程,是动态产生,动态消亡的(有一定的生命周期)
- 独立性:进程间空间独立,资源不共享,各自独立
- 进程是一个能够独立运行的基本单位,每个进程拥有独立的空间,各自使用自己空间资源
- 结构特征:进程由程序、进程控制块和数据三部分组成
- 面试要求
- 什么是进程,进程和程序有什么区别
进程是程序的一次执行,是一个动态过程的描述,有一定的生命周期
程序是一个可执行文件,是静态地占有磁盘空间,不占有计算机运行资源 - 进程有哪些状态,状态之间如何转化
就绪态
IO操作完成,等待cpu时间片
具备执行条件,等待cpu分配资源
运行态
在内存中开闭进程控制块
占用cpu时间片,执行进程
阻塞态
等待IO操作完成
进程停止执行,让出cpu
2.2 基于fork的多进程编程
fork使用
- 说明:fork基于linux系统,windows不具有
- 语法:pid = os.fork()
- 功能: 创建新的进程
- 返回值:整数,如果创建进程失败返回一个负数,如果成功则在原有进程中返回新进程的PID,在新进程中返回0
- 注意
- 子进程会复制父进程全部内存空间,从fork下一句开始执行。
- 父子进程各自独立运行,运行顺序不一定。
- 利用父子进程fork返回值的区别,配合if结构让父子进程执行不同的内容几乎是固定搭配
- 父子进程有各自特有特征比如PID PCB 命令集等。
- 父进程fork之前开辟的空间子进程同样拥有,父子进程对各自空间的操作不会相互影响。
进程相关函数
代码示例:day1/get_pid.py
代码示例:day1/exit.py
os.getpid()
功能: 获取一个进程的PID值
返回值: 返回当前进程的PIDos.getppid()
功能: 获取父进程的PID号
返回值: 返回父进程PIDos._exit(status)
功能: 结束一个进程
参数:进程的终止状态sys.exit([status])
功能:退出进程
参数:整数 表示退出状态
字符串 表示退出时打印内容
2.3 孤儿和僵尸
- 孤儿进程 : 父进程先于子进程退出,此时子进程成为孤儿进程。
特点: 孤儿进程会被系统进程收养,此时系统进程就会成为孤儿进程新的父进程,孤儿进程退出该进程会自动处理。
- 僵尸进程 :子进程先于父进程退出,父进程又没有处理子进程的退出状态,此时子进程就会称为僵尸进程。
- 僵尸进程虽然结束,但是会存留部分PCB在内存中。
- 影响:大量的僵尸进程会浪费系统的内存资源。
- 如何避免僵尸进程产生
- 使用wait函数处理子进程退出
代码示例:day1/wait.py
pid,status = os.wait()
功能:在父进程中阻塞等待处理子进程退出
返回值: pid 退出的子进程的PID
status 子进程退出状态
- 创建二级子进程处理僵尸:创建一个二级子进程来处理僵尸进程
代码示例:day1/child.py
【1】 父进程创建子进程,等待回收子进程
【2】 子进程创建二级子进程然后退出
【3】 二级子进程称为孤儿,和原来父进程一同执行事件 - 通过信号处理子进程退出
原理: 子进程退出时会发送信号给父进程,如果父进程忽略子进程信号,则系统就会自动处理子进程退出。
方法: 使用signal模块在父进程创建子进程前写如下语句 :
import signal signal.signal(signal.SIGCHLD,signal.SIG_IGN)
特点 : 非阻塞,不会影响父进程运行。可以处理所有子进程退出
练习:多人聊天器
2.4 multiprocessing模块创建进程
进程创建方法
代码示例:day2/process1.py
代码示例:day2/process2.py
代码示例:day2/process3.py
- 流程特点
【1】 将需要子进程执行的事件封装为函数
【2】 通过模块的Process类创建进程对象,关联函数
【3】 可以通过进程对象设置进程信息及属性
【4】 通过进程对象调用start启动进程
【5】 通过进程对象调用join回收进程 - 基本接口使用
Process()
功能 : 创建进程对象
参数 : target 绑定要执行的目标函数
args 元组,用于给target函数位置传参
kwargs 字典,给target函数键值传参
p.start()
功能 : 启动进程
注意:启动进程此时target绑定函数开始执行,该函数作为子进程执行内容,此时进程真正被创建
p.join([timeout])
功能:阻塞 等待回收进程(等待子进程结束后停止阻塞,有效地处理僵尸进程)
参数:超时时间
说明
- 使用multiprocessing创建进程同样是子进程复制父进程空间代码段,父子进程运行互不影响。
- 子进程只运行target绑定的函数部分,其余内容均是父进程执行内容。
- multiprocessing中父进程往往只用来创建子进程回收子进程,具体事件由子进程完成。
- multiprocessing创建的子进程中无法使用标准输入(无法使用input)
- 进程对象属性
代码示例:day2/process_attr.py
p.name 进程名称
p.pid 对应子进程的PID号
p.is_alive() 查看子进程是否在生命周期
p.daemon 设置父子进程的退出关系 (默认 daemon=False 表示子进程不随父进程退出而退出)
适用性:适用于主服务(父进程)退出时,其他依附于主服务的子服务(子进程)没必要继续运行的情况下。
- 如果设置为True则子进程会随父进程的退出而结束(daemon=True)
- 要求必须在start()前设置
- 如果daemon设置成True 通常就不会使用 join()
进程池实现
代码示例:day2/pool.py
- 技术产生的原因
- 进程的创建和销毁过程消耗的资源较多
- 当任务量众多,每个任务在很短时间内完成时,需要频繁的创建和销毁进程。此时对计算机压力较大
- 原理:创建一定数量的进程来处理事件,事件处理完进程不退出而是继续处理其他事件,直到所有事件全都处理完毕统一销毁。
- 类比:将一堆东西使用袋子运输到另一个地方,进程池的处理方式是,用几个袋子来回运输,直到运输完成,然后把袋子扔掉。
- 优点:增加进程的重复利用,降低资源消耗。
- 说明:进程池中进程间的通讯,需要使用multiprocess 中Manager类
- 进程池实现
【1】 创建进程池对象,放入适当的进程数
from multiprocessing import Pool
pool=Pool(processes)
功能: 创建进程池对象
参数: 指定进程数量,默认根据系统自动判定
【2】 将事件加入进程池队列执行
pool.apply_async(func,args,kwds)
功能: 使用进程池执行 func事件
参数: func 事件函数
args 元组 给func按位置传参
kwds 字典 给func按照键值传参
返回值: 返回函数事件对象
【3】 关闭进程池—停止往进程池添加事件
pool.close()
功能: 关闭进程池
【4】 回收进程池中进程
pool.join()
功能: 回收进程池中进程
2.5 进程间通信(IPC)
英文名:Inter Process Communication
- 目的:传输数据、共享数据、通知事件、资源共享、进程控制。
- 必要性:因为进程间空间独立,资源不共享,进程间通讯需要特定的手段。
- 常用进程间通信方法
管道 消息队列 共享内存 信号 信号量 套接字
管道通信(Pipe)
代码示例:day2/pipe.py
- 通信原理
在内存中开辟管道空间,生成管道操作对象,多个进程使用同一个管道对象进行读写
- 实现方法
from multiprocessing import Pipe
fd1,fd2 = Pipe(duplex = True)
功能: 创建管道
参数:默认表示双向管道(两端都可读写)
如果为False 表示单向管道
返回值:表示管道两端的读写对象
如果是双向管道均可读写
如果是单向管道fd1只读 fd2只写
fd.recv()
功能 : 从管道获取内容
返回值:获取到的数据
fd.send(data)
功能: 向管道写入内容
参数: 要写入的数据
消息队列
代码示例:day2/queue_0.py
- 通信原理
在内存中建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信。
- 特点:继承队列的先进先出的特点,可利用阻塞协调消息的收发速度
- 优点:可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便;
- 缺点:信息的复制需要额外消耗CPU的时间,不适宜于信息量大或操作频繁的场合
- 实现方法
from multiprocessing import Queue
q = Queue(maxsize=0)
功能: 创建队列对象
参数:最多存放消息个数
返回值:队列对象
q.put(data,[block,timeout])
功能:向队列存入消息
参数:data 要存入的内容
block 设置是否阻塞 False为非阻塞
timeout 超时检测
q.get([block,timeout])
功能:从队列取出消息
参数:block 设置是否阻塞 False为非阻塞
timeout 超时检测
返回值: 返回获取到的内容
q.full() 判断队列是否为满
q.empty() 判断队列是否为空
q.qsize() 获取队列中消息个数
q.close() 关闭队列
共享内存
代码示例:day3/value.py
代码示例:day3/array.py
- 通信原理:在内中开辟一块空间,进程可以写入和读取内容完成通信。
- 特点:每次写入内容会覆盖之前内容。
- 优点:无须复制,快捷,信息量大;
- 缺点:
- 通信是通过将共无法实现享空间缓冲区直接附加到进程的虚拟地址空间中来实现的,因此进程间的读写操作的同步问题;
- 利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信
- 实现方法
from multiprocessing import Value,Array
obj = Value(ctype,data)
功能 : 开辟共享内存(设置数据格式,初始值)
参数 : ctype 表示共享内存空间类型 'i' 'f' 'c'
data 共享内存空间初始数据
返回值:共享内存对象
obj.value 对该属性的修改查看 即对共享内存读写
obj = Array(ctype,data)
功能: 开辟共享内存空间
参数: ctype 表示共享内存数据类型
data 整数则表示开辟空间的大小,
其他数据类型表示开辟空间存放的初始化数据
返回值:共享内存对象
Array共享内存读写: 通过遍历obj可以得到每个值,直接可以通过索引序号修改任意值。
* 可以使用obj.value直接打印共享内存中的字节串
信号量(信号灯集)
代码示例:day2/sem.py
- 通信原理
给定一个数量,对多个进程可见。多个进程都可以操作该数量增减,并根据数量值决定自己的行为。
- 适用性:控制对共享资源的访问次数
- 实现方法:不同进程共同增减信号量,当信号量为0时阻塞
from multiprocessing import Semaphore
sem = Semaphore(num)
功能 : 创建信号量对象
参数 : 信号量的初始值
返回值 : 信号量对象
sem.acquire() 将信号量减1 当信号量为0时阻塞
sem.release() 将信号量加1
sem.get_value() 获取信号量数量
套接字:
可用于不同及其间的进程通信
优点:1)传输数据为字节级,传输数据可自定义,数据量小效率高;2)传输数据时间短,性能高;3) 适合于客户端和服务器端之间信息实时交互;4) 可以加密,数据安全性强
缺点:1) 需对传输的数据进行解析,转化成应用级的数据。
三、线程编程(Thread)
3.1 线程基本概念
- 什么是线程
- 线程被称为轻量级的进程,进程的分支任务
- 线程特征
- 线程也可以使用计算机多核资源,是多任务编程方式
- 线程是系统分配内核的最小单元
- 一个进程中的所有线程共享这个进程的资源,线程之间的运行互不影响
- 线程的创建和销毁消耗资源远小于进程
- 各个线程也有自己的ID等特征
- 优点:把进程中负责IO处理、人机交互而被阻塞的部分与密集计算别的部分分开执行,从而提高程序的执行效率
3.2 threading模块创建线程
代码示例:day3/thread1.py
代码示例:day3/thread2.py
【1】 创建线程对象
from threading import Thread
t = Thread()
功能:创建线程对象
参数:target 绑定线程函数
args 元组 给线程函数位置传参
kwargs 字典 给线程函数键值传参
【2】 启动线程:t.start()
【3】 回收线程: t.join([timeout])
3.3 线程对象属性
代码示例:day3/thread_attr.py
t.name 线程名称
t.setName() 设置线程名称
t.getName() 获取线程名称t.is_alive() 查看线程是否在生命周期
t.daemon 设置主线程和分支线程的退出关系
t.setDaemon() 设置daemon属性值
t.isDaemon() 查看daemon属性值daemon为True时主线程退出分支线程也退出。要在start前设置,通常不和join一起使用。
3.4 自定义线程类
代码示例:day3/myThread.py
- 原因:python封装的线程,每个线程只允许执行一个函数,参数的形式固定。
- 适用情况:需要改变参数,或是实现一些复杂的功能。功能复杂到一个函数无法完成,需要使用类进行封装。
- 创建步骤
【1】 继承Thread类
【2】 重写__init__方法添加自己的属性,使用super加载父类属性
【3】 重写run方法(线程对象使用start方法时,会调用run方法) - 使用方法
【1】 实例化对象
【2】 调用start自动执行run方法
【3】 调用join回收线程
3.5 同步互斥
线程间通信方法
- 通信方法:线程间使用全局变量进行通信
- 共享资源争夺问题
- 共享资源:多个进程或者线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临界区。
- 影响 : 对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机制协调操作顺序。
- 同步互斥机制
- 同步 : 同步是一种协作关系,为完成操作,多进程或者线程间形成一种协调,按照必要的步骤有序执行操作。
2. 互斥 : 互斥是一种制约关系,当一个进程或者线程占有资源时会进行加锁处理,此时其他进程线程就无法操作该资源,直到解锁后才能操作。
3.6 线程同步互斥方法
线程Event
代码示例:day3/thread_event.py
from threading import Event
e = Event() 创建线程event对象
e.wait([timeout]) 阻塞等待e被set
e.set() 设置e,使wait结束阻塞
e.clear() 使e回到未被设置状态
e.is_set() 查看当前e是否被设置
线程锁 Lock
代码示例:day3/thread_event.py
方法一:
from threading import Lock
lock = Lock() 创建锁对象
lock.acquire() 上锁 如果lock已经上锁再调用会阻塞
lock.release() 解锁
方法二:
with lock: 上锁
...
...
with代码块结束自动解锁
死锁及其处理
- 定义:类比两方都占用俘虏,又想对方释放俘虏,互不让步,僵持在那
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
- 死锁产生条件
代码示例: day3/dead_lock.py
死锁发生的必要条件
- 互斥条件:指线程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求线程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放,通常CPU内存资源是可以被系统强行调配剥夺的。
- 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即进程集合{T0,T1,T2,···,Tn}中的T0正在等待一个T1占用的资源;T1正在等待T2占用的资源,……,Tn正在等待已被T0占用的资源。
死锁的产生原因
简单来说造成死锁的原因可以概括成三句话:
- 当前线程拥有其他线程需要的资源
- 当前线程等待其他线程已拥有的资源
- 都不放弃自己拥有的资源
- 如何避免死锁
死锁是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生。通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现的方法。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率。
3.7 python线程GIL
英文:Global Interpreter Lock
python线程的GIL问题 (全局解释器锁)
- 什么是GIL :由于CPython解释器设计中加入了解释器锁,导致同一时刻只能解释执行一个线程,大大降低了线程的执行效率。
- 特点:当遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行多阻塞高延迟IO时可以提升程序效率,其他情况并不能对效率有所提升。
- GIL问题建议
- 尽量使用进程完成无阻塞的并发行为
- 不使用C作为解释器 (Java C#解释器没有GIL,CPython解释器执行效率更高)
- 结论 :
- 只有IO Bound场景(由IO密集型程序导致的阻塞,高延迟多阻塞的IO密集型)下多线程会得到较好的性能
- 在无阻塞状态下,多线程程序和单线程程序执行效率几乎差不多,甚至还不如单线程效率。但是多进程运行相同内容却可以有明显的效率提升。
四、进程VS线程
区别
- 进程的创建删除消耗的计算机资源比线程多
- 进程空间独立,数据互不干扰,有专门通信方法;线程使用全局变量通信
- 多个线程共享进程资源,在共享资源操作时往往需要同步互斥处理
- 进程是资源的分配和调度的一个基本单元,而线程是CPU调度的基本单元
联系
- 一个进程可以有多个分支线程,两者有包含关系
- 两者都是多任务编程方式,都能使用计算机多核资源
- 进程线程在系统中都有自己的特有属性标志,如ID,代码段,命令集等。
使用场景
- 任务场景:如果是相对独立的任务模块,可能使用多进程,如果是多个分支共同形成一个整体任务可能用多线程
- 项目结构:多种编程语言实现不同任务模块,可能是多进程,或者前后端分离应该各自为一个进程。
- 难易程度:通信难度,数据处理的复杂度来判断用进程间通信还是同步互斥方法。
- 具体场景
1)需要频繁创建销毁的优先用线程
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
2)需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
3)强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
4)可能要扩展到多机分布的用进程,多核分布的用线程
要求
- 对进程线程怎么理解/说说进程线程的差异
- 进程间通信知道哪些,有什么特点
- 什么是同步互斥,你什么情况下使用,怎么用
- 给一个情形,说说用进程还是线程,为什么
- 问一些概念,僵尸进程的处理,GIL问题,进程状态
五、IO并发
5.1 IO 分类
IO分类:阻塞IO ,非阻塞IO,IO多路复用,异步IO等
阻塞IO
1.定义:执行条件不满足被阻塞的IO事件。阻塞IO是IO的默认形态。
2.效率:阻塞IO是效率很低的一种IO。但是由于逻辑简单所以是默认IO行为。
3.阻塞情况:
- 因为某种执行条件没有满足造成的函数阻塞
e.g. accept input recv sleep - 处理IO的时间较长产生的阻塞状态
e.g. 网络传输,大文件读写
非阻塞IO
- 定义 :通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。
- 意义:一定程度上提高IO程序运行效率
- 原理:提前执行与IO操作无关事件,再返回阻塞,等待阻塞条件达成再执行IO操作相关事件
- 设置套接字为非阻塞IO
sockfd.setblocking(bool)
功能:设置套接字为非阻塞IO
参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞
- 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。
sockfd.settimeout(sec)
功能:设置套接字的超时时间
参数:设置的时间
5.2 IO多路复用
- 定义:同时监控多个IO事件(由计算机内核操作),当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的能力。
- 过程:应用层将多个IO事件给计算机内核监控,当哪个IO事件准备就绪就时内核告知应用层执行这个IO事件
- 作用:避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
- 优点:IO多路复用消耗资源较少,效率较高
- 具体方案
- select方法 :windows linux unix
- poll方法:linux unix
- epoll方法:linux
- 特点对比:
- select支持多平台,最大监控数量少(只有1024)
- epoll 执行效率比select,poll要高
- epoll 、poll 监控IO数量比select要多(10万左右)
- epoll 的触发方式比poll要丰富 (EPOLLET边缘触发,select和poll是水平触发)
select 方法编程实现
代码实现: day5/select_server.py
rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能: 监控IO事件,阻塞等待IO发生
参数:rlist 列表 存放关注的等待发生的IO事件 等待被访问的IO事件对象、
wlist 列表 存放关注的要主动处理的IO事件 写操作IO事件
xlist 列表 存放关注的出现异常要处理的IO
timeout 超时时间
说明:IO事件的分类 是相对服务器来讲 等待客户端访问是读(接收) 向客户端发送信息是写(发送)
返回值: rs 列表 rlist中准备就绪的IO
ws 列表 wlist中准备就绪的IO
xs 列表 xlist中准备就绪的IO
select 实现tcp服务步骤
【1】将关注的IO放入对应的监控类别列表
【2】通过select函数进行监控
【3】遍历select返回值列表,确定就绪IO事件
【4】处理发生的IO事件
说明:
wlist中如果存在IO事件,则select立即返回给ws
处理IO过程中不要出现死循环占有服务端的情况
IO多路复用消耗资源较少,效率较高
扩展: 位运算
定义 : 将整数转换为二进制,按二进制位进行运算
应用:一种新类属性表达方式,使用二进制表示一个对象的多个属性
如:用户权限表达(属性取值为bool类型)
运算符号:
& 按位与
| 按位或
^ 按位异或
<< 左移右移
e.g. 14 --> 01110
19 --> 10011
14 & 19 = 00010 = 2 一0则0
14 | 19 = 11111 = 31 一1则1
14 ^ 19 = 11101 = 29 相同为0不同为1
14 << 2 = 111000 = 56 向左移动低位补0
14 >> 2 = 11 = 3 向右移动去掉低位
poll方法
代码实现: day5/poll_server.py
p = select.poll()
功能 : 创建poll对象
返回值: poll对象
p.register(fd,event)
功能: 注册关注的IO事件
参数:fd 要关注的IO
event 要关注的IO事件类型
常用类型:POLLIN 读IO事件(rlist)
POLLOUT 写IO事件 (wlist)
POLLERR 异常IO (xlist)
POLLHUP 断开连接
例如: p.register(sockfd,POLLIN|POLLERR)
p.unregister(fd)
功能:取消对IO事件的关注
参数:IO对象或者IO对象的fileno
events = p.poll()
功能: 阻塞等待监控的IO事件发生
返回值: 返回发生的IO
events格式 [(fileno,event),()....]
每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
poll_server 步骤
【1】 创建套接字
【2】 将套接字register
【3】 创建查找字典,并维护(要时刻与注册IO保持一致)
【4】 循环监控IO发生
【5】 处理发生的IO
epoll方法
代码实现: day5/epoll_server.py
- 使用方法 : 基本与poll相同
- 将生成对象改为 epoll()
- 将所有事件类型改为EPOLL类型
- 特点
- epoll 效率比select,poll要高
- epoll 、poll 监控IO数量比select要多(10万左右)
- epoll 的触发方式比poll要多 (EPOLLET边缘触发,select和poll是水平触发)
5.3 协程技术
基础概念
- 定义:
- 纤程,微线程。
- 是为非抢占式(线程和进程是抢占式)多任务产生子程序的计算机组件(程序)。
- 协程允许不同入口点在不同位置暂停或开始(协程就是可以暂停执行的函数,如生成器具有该特点)
- 应用:提高程序的运行效率
- 对比:
- 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
- 一个进程可以包含多个线程,一个线程可以包含多个协程。
- 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
- 协程原理:记录一个函数的上下文栈帧,协程调度切换时会将记录的上下文保存,在切换回来时进行调取,恢复原有的执行内容,以便从上一次执行位置继续执行。(函数的上下文来回切换)
- 协程优缺点
- 优点:高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
- 协程完成多任务占用计算资源很少
- 由于协程的多任务切换在应用层完成,因此切换开销少
- 协程为单线程程序,无需进行共享资源同步互斥处理
- 缺点:协程的本质是一个单线程,无法利用计算机多核资源
扩展延伸@标准库协程的实现
python3.5以后,使用标准库asyncio和async/await 语法来编写并发代码。asyncio库通过对异步IO行为的支持完成python的协程。虽然官方说asyncio是未来的开发方向,但是由于其生态不够丰富,大量的客户端不支持awaitable需要自己去封装,所以在使用上存在缺陷。更多时候只能使用已有的异步库(asyncio等),功能有限
第三方协程模块
- greenlet模块
示例代码: day6/greenlet_0.py
- 安装 : sudo pip3 install greenlet
- 函数
greenlet.greenlet(func)
功能:创建协程对象
参数:协程函数
g.switch()
功能:选择要执行的协程函数
- gevent模块
示例代码: day6/gevent_test.py
示例代码: day6/gevent_server.py
- 安装:sudo pip3 install gevent
- 函数
gevent.spawn(func,argv)
功能: 生成协程对象
参数:func 协程函数
argv 给协程函数传参(不定参)
返回值: 协程对象
gevent.joinall(list,[timeout])
功能: 阻塞等待协程执行完毕
参数:list 协程对象列表
timeout 超时时间
gevent.sleep(sec)
功能: gevent睡眠阻塞
参数:睡眠时间
* gevent协程只有在遇到gevent指定的阻塞行为时才会自动在协程之间进行跳转
如gevent.joinall(),gevent.sleep()带来的阻塞
- monkey脚本
作用:在gevent协程中,协程只有遇到gevent指定类型的阻塞才能跳转到其他协程,因此,我们希望将普通的IO阻塞行为转换为可以触发gevent协程跳转的阻塞,以提高执行效率。
转换方法:gevent 提供了一个脚本程序monkey,可以修改底层解释IO阻塞的行为,将很多普通阻塞转换为gevent阻塞。
- 使用方法
【1】 导入monkey
from gevent import monkey
【2】 运行相应的脚本,例如转换socket中所有阻塞
monkey.patch_socket()
【3】 如果将所有可转换的IO阻塞全部转换则运行all
monkey.patch_all()
【4】 注意:脚本运行函数需要在对应模块导入前执行
六、并发网络通信模型
6.1 常见模型分类
- 循环服务器模型 :循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。
优点:实现简单,占用资源少
缺点:无法同时处理多个客户端请求
适用情况:处理的任务可以很快完成,客户端无需长期占用服务端程序。udp比tcp更适合循环。
- IO并发模型:利用IO多路复用,异步IO等技术,同时处理多个客户端IO请求。
优点 : 资源消耗少,能同时高效处理多个IO行为
缺点 : 只能处理并发产生的IO事件,无法处理cpu计算
适用情况:HTTP请求,网络传输等都是IO行为。
- 多进程/线程网络并发模型:每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程。
优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。
缺点: 资源消耗较大
适用情况:客户端同时连接量较少,需要处理行为较复杂情况。
6.2 基于fork的多进程网络并发模型
代码实现: day4/fork_server.py
实现步骤
- 创建监听套接字
- 等待接收客户端请求
- 客户端连接创建新的进程处理客户端请求
- 原进程继续等待其他客户端连接
- 如果客户端退出,则销毁对应的进程
6.3 基于threading的多线程网络并发
代码实现: day4/thread_server.py
实现步骤
- 创建监听套接字
- 循环接收客户端连接请求
- 当有新的客户端连接创建线程处理客户端请求
- 主线程继续等待其他客户端连接
- 当客户端退出,则对应分支线程退出
6.4 ftp 文件服务器
代码实现: day5/ftp
- 功能
【1】 分为服务端和客户端,要求可以有多个客户端同时操作。
【2】 客户端可以查看服务器文件库中有什么文件。
【3】 客户端可以从文件库中下载文件到本地。
【4】 客户端可以上传一个本地文件到文件库。
【5】 使用print在客户端打印命令输入提示,引导操作
6.5 HTTPServer v2.0
day6/http_server.py
- 主要功能 :
【1】 接收客户端(浏览器)请求
【2】 解析客户端发送的请求
【3】 根据请求组织数据内容
【4】 将数据内容形参http响应格式返回给浏览器 - 升级点 :
【1】 采用IO并发,可以满足多个客户端同时发起请求情况
【2】 做基本的请求解析,根据具体请求返回具体内容,同时满足客户端简单的非网页请求情况
【3】 通过类接口形式进行功能封装