导图社区 多任务编程
达内培训_python_第二阶段内容模块二
编辑于2019-05-17 10:43:55多任务编程
通过应用程序利用计算机的多个核心达到同时执行多个任务的目的,一次提高计算机运行效率
意义
充分利用计算机的资源提高程序的运行效率
实施方案
并发
同时处理多个任务,内核在多个任务间不断的切换,达到好像都在处理运行的效果。但实际一个时间点内核只能处理其中一个任务。
循环服务器模型
优点:实现简单,占用资源少 缺点:无法同时处理多个客户端请求 适用情况:处理的任务可以很快完成,客户端无需长期占用服务端程序。udp比tcp更适合循 环。
IO并发模型
优点 : 资源消耗少,能同时高效处理多个IO行为 缺点 : 只能处理并发产生的IO事件,无法处理cpu计算 适用情况:HTTP请求,网络传输等都是IO行为。
阻塞IO
1.定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。 2.效率:阻塞IO是效率很低的一种IO。但是由于逻辑简单所以是默认IO行为。 3.阻塞情况: 因为某种执行条件没有满足造成的函数阻塞 e.g. accept input recv 处理IO的时间较长产生的阻塞状态 e.g. 网络传输,大文件读写
非阻塞IO
1. 定义 :通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。 设置套接字为非阻塞IO sockfd.setblocking(bool) 功能:设置套接字为非阻塞IO 参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞 超时检测 :设置一个最长阻塞时间,超过该时间后则不再阻塞等待。 sockfd.settimeout(sec) 功能:设置套接字的超时时间 参数:设置的时间
IO多路复用
同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO 的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
select方法
适用windows linux unixrs, ws, xs=select(rlist, wlist, xlist[, timeout]) 功能: 监控IO事件,阻塞等待IO发生 参数:rlist 列表 存放关注的等待发生的IO事件 wlist 列表 存放关注的要主动处理的IO事件 xlist 列表 存放关注的出现异常要处理的IO timeout 超时时间 返回值: 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多路复用消耗资源较少,效率较高
poll方法
linux unix p = select.poll() 功能 : 创建poll对象 返回值: poll对象 p.register(fd,event) 功能: 注册关注的IO事件 参数:fd 要关注的IO event 要关注的IO事件类型 常用类型:POLLIN 读IO事件(rlist) POLLOUT 写IO事件 (wlist) POLLERR 异常IO (xlist) POLLHUP 断开连接 e.g. p.register(sockfd,POLLIN|POLLERR) p.unregister(fd) 功能:取消对IO的关注 参数:IO对象或者IO对象的fileno events = p.poll() 功能: 阻塞等待监控的IO事件发生 返回值: 返回发生的IO events格式 [(fileno,event),()....] 每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型
位运算
定义 : 将整数转换为二进制,按二进制位进行运算 & | ^ << >> e.g. 14 14 14 14 14 按位与 按位或 按位异或 左移 右移 14 --> 01110 19 --> 10011 & 19 | 19 ^ 19 << 2 >> 2 = = = = = 00010 = 2 一0则0 11111 = 31 一1则1 11101 = 29 相同为0不同为1 111000 = 56 向左移动低位补0 11 = 3 向右移动去掉低位
epoll方法
linux 1. 使用方法 : 基本与poll相同 生成对象改为 epoll() 将所有事件类型改为EPOLL类型 2. epoll特点 epoll 效率比select poll要高 epoll 监控IO数量比select poll要多 epoll 的触发方式比poll要多 (EPOLLET边缘触发)
异步IO
调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。E同学让舍管阿姨将杯子装满水后通知他。整个过程E同学都可以做别的事情(没有recv),这才是真正的异步IO。
多进程/线程网络并发模型
优点:能同时满足多个客户端长期占有服务端需求,可以处理各种请求。 缺点: 资源消耗较大 适用情况:客户端同时连接量较少,需要处理行为较复杂情况。
进程
程序在计算机中的一次运行。
特征
进程的运行特征 【1】 进程可以使用计算机多核资源 【2】 进程是计算机分配资源的最小单位 【3】 进程之间的运行互不影响,各自独立 【4】 每个进程拥有独立的空间,各自使用自己空间资源
产生过程
【1】 用户空间通过调用程序接口或者命令发起请求 【2】 操作系统接收用户请求,开始创建进程 【3】 操作系统调配计算机资源,确定进程状态等 【4】 操作系统将创建的进程提供给用户使用
相关概念
1.cpu时间片:如果一个进程占有cpu内核则称这个进程在cpu时间片上。 2.PCB(进程控制块):在内存中开辟的一块空间,用于存放进程的基本信息,也用于系统查找识别进 程。 3.进程ID(PID): 系统为每个进程分配的一个大于0的整数,作为进程ID。每个进程ID不重复。 Linux查看进程ID : ps -aux 4.父子进程 : 系统中每一个进程(除了系统初始化进程)都有唯一的父进程,可以有0个或多个子进 程。父子进程关系便于进程管理。 查看进程树: pstree
进程状态
三态
三态 1.就绪态 : 进程具备执行条件,等待分配cpu资源 2.运行态 : 进程占有cpu时间片正在运行 3.等待态 : 进程暂时停止运行,让出cpu
五态
三态 1.新建 : 创建一个进程,获取资源的过程 2.就绪态 : 进程具备执行条件,等待分配cpu资源 3.运行态 : 进程占有cpu时间片正在运行 4.等待态 : 进程暂时停止运行,让出cpu 5.终止 : 进程结束,释放资源的过程
相关函数
os.getpid() 功能: 获取一个进程的PID值 返回值: 返回当前进程的PID os.getppid() 功能: 获取父进程的PID号 返回值: 返回父进程PID os._exit(status) 功能: 结束一个进程 参数:进程的终止状态 sys.exit([status]) 功能:退出进程 参数:整数 表示退出状态 字符串 表示退出时打印内容
实现方法
fork---适用于Linux/Unix/MacOS系统,windows不能用 multiprocessing---所有平台
基于fork
代码实现
pid = os.fork() 功能: 创建新的进程 返回值:整数,如果创建进程失败返回一个负数,如果成功则在原有进程中返回新进程的PID,在 新进程中返回0 注意 子进程会复制父进程全部内存空间,从fork下一句开始执行。 父子进程各自独立运行,运行顺序不一定。 利用父子进程fork返回值的区别,配合if结构让父子进程执行不同的内容几乎是固定搭 配。 父子进程有各自特有特征比如PID PCB 命令集等。 父进程fork之前开辟的空间子进程同样拥有,父子进程对各自空间的操作不会相互影 响。
基于multiprocessing
创建方法
流程特点
【1】 将需要子进程执行的事件封装为函数 【2】 通过模块的Process类创建进程对象,关联函数 【3】 可以通过进程对象设置进程信息及属性 【4】 通过进程对象调用start启动进程 【5】 通过进程对象调用join回收进程
基本接口的使用
Process() 功能 : 创建进程对象 参数 : target 绑定要执行的目标函数 args 元组,用于给target函数位置传参 kwargs 字典,给target函数键值传参 p.start() 功能 : 启动进程 注意:启动进程此时target绑定函数开始执行,该函数作为子进程执行内容,此时进程真正被创建 p.join([timeout]) 功能:阻塞等待回收进程 参数:超时时间 注意 使用multiprocessing创建进程同样是子进程复制父进程空间代码段,父子进程运行互不 影响。 子进程只运行target绑定的函数部分,其余内容均是父进程执行内容。 multiprocessing中父进程往往只用来创建子进程回收子进程,具体事件由子进程完成。 multiprocessing创建的子进程中无法使用标准输入
进程对象属性
p.name 进程名称 p.pid 对应子进程的PID号 p.is_alive() 查看子进程是否在生命周期 p.daemon 设置父子进程的退出关系 如果设置为True则子进程会随父进程的退出而结束 要求必须在start()前设置 如果daemon设置成True 通常就不会使用 join()
进程池
必要性
【1】 进程的创建和销毁过程消耗的资源较多 【2】 当任务量众多,每个任务在很短时间内完成时,需要频繁的创建和销毁进程。此时对计算 机压力较大 【3】 进程池技术很好的解决了以上问题。
原理
创建一定数量的进程来处理事件,事件处理完进 程不退出而是继续处理其他事件,直到所有事件 全都处理完毕统一销毁。增加进程的重复利用,降低资源消耗。
实现步骤
【1】 创建进程池对象,放入适当的进程
from multiprocessing import Pool Pool(processes) 功能: 创建进程池对象 参数: 指定进程数量,默认根据系统自动判定
【2】 将事件加入进程池队列执行
pool.apply_async(func,args,kwds) 功能: 使用进程池执行 func事件 参数: func 事件函数 args 元组 kwds 字典 返回值: 返回函数事件对象 给func按位置传参 给func按照键值传参
【3】 关闭进程池
pool.close() 功能: 关闭进程池
【4】 回收进程池中进程
pool.join() 功能: 回收进程池中进程
进程间的通信
必要性
必要性: 进程间空间独立,资源不共享,此时在需要进程间数据传输时就需要特定的手段进行数 据通信。
实现方法
管道通信(Pipe)
原理
在内存中开辟管道空间,生成管道操作对象,多个进程使用同一个管道对象进行读写即可实现通 信
方法
from multiprocessing import Pipe fd1,fd2 = Pipe(duplex = True) 功能: 创建管道 参数:默认表示双向管道 如果为False 表示单向管道 返回值:表示管道两端的读写对象 如果是双向管道均可读写 如果是单向管道fd1只读 fd2只写 fd.recv() 功能 : 从管道获取内容 返回值:获取到的数据 fd.send(data) 功能: 向管道写入内容 参数: 要写入的数据
消息队列(Queue)
原理
在内存中建立队列模型,进程通过队列将消息存入,或者从队列取出完成进程间通信
方法
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() 判断队列是否为满 判断队列是否为空 获取队列中消息个数 关闭队列
共享内存
原理
在内中开辟一块空间,进程可以写入内容和读取内容完成通信,但是每次写入内容会 覆盖之前内容。
方法
from multiprocessing import Value,Array obj = Value(ctype,data) 功能 : 开辟共享内存 参数 : ctype 表示共享内存空间类型 'i' data 共享内存空间初始数据 返回值:共享内存对象 obj.value 'f' 'c' 对该属性的修改查看即对共享内存读写 obj = Array(ctype,data) 功能: 开辟共享内存空间 参数: ctype 表示共享内存数据类型 data 整数则表示开辟空间的大小,其他数据类型 返回值:共享内存对象 表示开辟空间存放 Array共享内存读写: 通过遍历obj可以得到每个值,直接可以通过索引序号修改任意值。 * 可以使用obj.value直接打印共享内存中的字节串
本地套接字
原理
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
方法
创建本地套接字流程 【1】 创建本地套接字 sockfd = socket(AF_UNIX,SOCK_STREAM) 【2】 绑定本地套接字文件 sockfd.bind(file) 【3】 监听,接收客户端连接,消息收发 listen()-->accept()-->recv(),send()
信号量(信号灯集)
原理
给定一个数量对多个进程可见。多个进程都可以操作该数量增减,并根据数量值决定自己的行 为。
方法
from multiprocessing import Semaphore sem = Semaphore(num) 功能 : 创建信号量对象 参数 : 信号量的初始值 返回值 : 信号量对象 sem.acquire() 将信号量减1 当信号量为0时阻塞 sem.release() 将信号量加1 sem.get_value() 获取信号量数量
缺点
孤儿进程
定义:父进程先于子进程退出,此时子进程成为孤儿进程。 特点: 孤儿进程会被系统进程收养,此时系统进程就会成为孤儿进程新的父进程,孤儿进程退出 该进程会自动处理。
僵尸进程
子进程先于父进程退出,父进程又没有处理子进程的退出状态,此时子进程就会称为 僵尸进程。
影响
影响: 僵尸进程虽然结束,但是会存留部分PCB在内存中,大量的僵尸进程会浪费系统的内存资 源。
处理方法
使用wait函数处理子进程退出
``` pid,status = os.wait() 功能:在父进程中阻塞等待处理子进程退出 返回值: pid 退出的子进程的PID status 子进程退出状态 pid,status = os.waitpid(pid,option) 功能: 在父进程中处理子进程退出状态 参数: pid -1 表示等待任意子进程退出 >0 表示等待指定的子进程退出 option 0 表示阻塞等待 WNOHANG 表示非阻塞 返回值:pid 退出的子进程的PID status 子进程退出状态
创建二级子进程处理僵尸
【1】 父进程创建子进程,等待回收子进程 【2】 子进程创建二级子进程然后退出 【3】 二级子进程称为孤儿,和原来父进程一同执行事件 适用于所有平台
通过信号处理子进程退出
原理: 子进程退出时会发送信号给父进程,如果父进程忽略子进程信号,则系统就会自动处 理子进程退出。 方法: 使用signal模块在父进程创建子进程前写如下语句 : import signal signal.signal(signal.SIGCHLD,signal.SIG_IGN) 特点 : 非阻塞,不会影响父进程运行。可以处理所有子进程退出 只适用于LINUX
线程
定义: 【1】 线程被称为轻量级的进程 【2】 线程也可以使用计算机多核资源,是多任务编程方式 【3】 线程是系统分配内核的最小单元 【4】 线程可以理解为进程的分支任务
特征
【1】 一个进程中可以包含多个线程 【2】 线程也是一个运行行为,消耗计算机资源 【3】 一个进程中的所有线程共享这个进程的资源 【4】 多个线程之间的运行互不影响各自运行 【5】 线程的创建和销毁消耗资源远小于进程 【6】 各个线程也有自己的ID等特征
实现方法
【1】 创建线程对象 from threading import Thread t = Thread() 功能:创建线程对象 参数:target 绑定线程函数 args 元组 给线程函数位置传参 kwargs 字典 给线程函数键值传参 【2】 启动线程 t.start() 【3】 回收线程 t.join([timeout])
相关函数
t.name 线程名称 t.setName() 设置线程名称 t.getName() 获取线程名称 t.is_alive() 查看线程是否在生命周期 t.daemon 设置主线程和分支线程的退出关系 t.setDaemon() 设置daemon属性值 t.isDaemon() 查看daemon属性值 daemon为True时主线程退出分支线程也退出。要在start前设置,通常不和join一起使用。
自定义线程类
1. 创建步骤 【1】 继承Thread类 【2】 重写__init__方法添加自己的属性,使用super加载父类属性 【3】 重写run方法 2. 使用方法 【1】 实例化对象 【2】 调用start自动执行run方法 【3】 调用join回收线程
线程间的通信
线程间使用全局变量进行通信
同步互斥
同步
同步 : 同步是一种协作关系,为完成操作,多进程或者线程间形成一种协调,按照必要的步骤有 序执行操作
互斥
互斥 : 互斥是一种制约关系,当一个进程或者线程占有资源时会进行加锁处理,此时其他进程线 程就无法操作该资源,直到解锁后才能操作。
解决方法
线程Event
from threading import Event e = Event() 创建线程event对象 e.wait([timeout]) e.set() 阻塞等待e被set 设置e,使wait结束阻塞 e.clear() 使e回到未被设置状态 e.is_set() 查看当前e是否被设置
线程锁 Lock
from threading import Lock lock = Lock() 创建锁对象 lock.acquire() 上锁 如果lock已经上锁再调用会阻塞 lock.release() 解锁 with ... ... lock: 上锁 with代码块结束自动解锁
死锁
死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻 塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死 锁。
产生条件
死锁发生的必要条件 1.互斥条件:指线程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个 进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程 用毕释放。 2.请求和保持条件:指线程已经保持至少一个资源,但又提出了新的资源请求,而该资源 已被其它进程占有,此时请求线程阻塞,但又对自己已获得的其它资源保持不放。 3.不剥夺条件:指线程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由 自己释放,通常CPU内存资源是可以被系统强行调配剥夺的。 4.环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即进程集合 {T0,T1,T2,···,Tn}中的T0正在等待一个T1占用的资源;T1正在等待T2占用的资 源,......,Tn正在等待已被T0占用的资源。
产生原因
简单来说造成死锁的原因可以概括成三句话: 当前线程拥有其他线程需要的资源 当前线程等待其他线程已拥有的资源 都不放弃自己拥有的资源
如何避免
死锁是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生。通过设置某些限制条 件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。预防死锁是一种较易实现 的方法。但是由于所施加的限制条件往往太严格,可能会导致系统资源利用率。 1.使用定时锁 2.使用重入锁RLock(),用法同Lock。RLock内部维护着一个Lock和一个counter变量,counter记录了 acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其 他的线程才能获得资源。
共享资源的争夺
共享资源:多个进程或者线程都可以操作的资源称为共享资源。对共享资源的操作代码段称为临 界区。 影响 : 对共享资源的无序操作可能会带来数据的混乱,或者操作错误。此时往往需要同步互斥机 制协调操作顺序。
python线程的GIL问题 (全局解释器锁)
由于python解释器设计中加入了解释器锁,导致python解释器同一时刻只能解释执 行一个线程,大大降低了线程的执行效率。
影响
1.由于每个CPU在同一时间之内执行一个线程,单核CPU下的多线程也只是并发, 不是并行, 2.因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以python多线程在执行 多阻塞高延迟IO时可以提升程序效率,其他情况并不能对效率有所提升。 3.在无阻塞状态下,多线程程序和单线程程序执行效率几乎差不多,甚至还不如单线程效 率。但是多进程运行相同内容却可以有明显的效率提升
建议
官方建议: 1.尽量使用进程完成无阻塞的并发行为 2.不使用c作为解释器 (Java C#)
集成模块完成多进程/线程网络并发
并行
多个计算机核心在同时处理多个任务,这时多个任务间是并行关系。
协程技术
定义:纤程,微线程。是为非抢占式多任务产生子程序的计算机组件。协程允许不同入口点在不 同位置暂停或开始,简单来说,协程就是可以暂停执行的函数。 2. 协程原理 : 记录一个函数的上下文栈帧,协程调度切换时会将记录的上下文保存,在切换回来时 进行调取,恢复原有的执行内容,以便从上一次执行位置继续执行。 3. 协程优缺点 优点 1. 协程完成多任务占用计算资源很少 2. 由于协程的多任务切换在应用层完成,因此切换开销少 3. 协程为单线程程序,无需进行共享资源同步互斥处理 缺点 协程的本质是一个单线程,无法利用计算机多核资源