多任务编程
- 意义:充分利用计算机多核资源,提高程序的运行效率,一个程序中同时使用多个任务。
- 实现方案:多进程,多线程
- 并发与并行
并发:同时处理多个任务,内核在任务间不断的切换达到好像多个任务同时执行的效果,实际每个时刻只有一个任务占有内核。
并行:多个任务利用计算机多核资源在同时执行,此时多个任务间并行关系。
进程(process)
进程理论基础
-
定义:程序在计算机中的一次运行。
程序是一个可执行的文件,是静态的占有磁盘。
进程是一个动态的过程描述,占有计算机运行资源,有一定的生命周期(开始运行生成进程,结束运行结束进程)。 -
系统中如何产生一个进程
【1】用户控件通过调用程序接口或者命令发起请求(应用层发起请求)
【2】操作系统接收用户请求,开始创建进程(操作系统接收请求)
【3】操作系统调配计算机硬件资源,确定进程状态等
【4】操作系统将创建的进程提供给用户使用 -
进程基本概念
-
cpu时间片:如果一个进程占有cpu内核则称这个进程在cpu时间片上。
-
PCB(进程控制块):在内存中开辟的一块控件,用于存放进程的基本信息,也用于系统查找识别进程。
-
进程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 有多线程的 -
进程的运行特征
【1】进程可以使用计算机多核资源
【2】进程是计算机分配资源的最小单位
【3】进程之间的运行互不影响,各自独立
【4】每个进程拥有独立的空间,各自使用自己空间资源 -
面试要求
- 什么是进程,进程和程序有什么区别
- 进程有哪些状态,状态之间如何转化
fork模块
os模块的多进程,创建进程,返回创建进程的子进程的进程ID,即pid。
孤儿进程
子进程还没结束,但是父进程已经结束了,这时候子进程没有对应的父进程,就称为了孤儿进程,系统会有一个专门回收孤儿进程的父进程,在linux系统开机后就存在。
僵尸进程
- 僵尸进程的处理方法
- os.wait
- 创建二级子进程
- 信号函数
import signal
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
作业:
- fork理解和使用
- IO函数总结
- 群聊思路
multiprocessing多进程
from multiprocessing import Process
from time import sleep
import os
def th1():
sleep(2)
print("吃饭")
print(os.getppid(),'----',os.getpid())
def th2():
sleep(2)
print("睡觉")
print(os.getppid(),'----',os.getpid())
def th3():
sleep(2)
print("打豆豆")
print(os.getppid(),'----',os.getpid())
if __name__ == '__main__':
things = [th1,th2,th3]
jobs = []
for th in things:
p = Process(target=th) # 可以用args元组的位置传参,也可以用kwargs用字典键值传参
p.start()
jobs.append(p) # 将进程对象保存
for i in jobs: # 遍历进程列表,回收进程
i.join()
muutiprocessing传参
from multiprocessing import Process
from time import sleep
def worker(sec,name):
for i in range(3):
sleep(sec)
print("I'm %s"%name)
print("i am working")
if __name__ == '__main__':
# args传参
p = Process(target=worker,args=(2,'sunhao'))
# kwargs传参
# p = Process(target=worker,kwargs={'sec':2,'name':'sunhao'})
p.start()
p.join()
进程间的互相访问(IPC)
管道通信
函数:Pipe()/send()/recv()
fd1,fd2 = Pipe(duplex = True) #默认True,双向通道,fd1写的只能是fd2读,False,单向通道,一个只能读,一个只能写
fd1.send()
fd2.recv()
消息队列:Queue()/q.get()/q.put()
共享内存:Value() Array()
信号量:Semaphore()/acquire()/release()
进程池
原理:创建一定数量的进程来处理事件,事件处理完成进程不退出,而是继续处理其他事件,直到所有事件全部处理完毕统一销毁,增加进程的重复利用,降低资源消耗。
就是因为原始的多进程方式会不断的创建进程,关闭进程,造成资源浪费,但是进程池就是创建进程后就创建好的这几个进程来回执行任务,直到任务完成,不会频繁创建和关闭。
Windows下实现进程池的代码,注意,建立进程池必须在main下,否则在Windows下运行不起来,具体原因参照:一吱大懒虫的博客 https://blog.csdn.net/qq_36708806/article/details/79731276
from multiprocessing import Pool
from time import ctime,sleep
# 进程池事件
def worker(msg):
sleep(2)
print(ctime(),'--',msg)
if __name__ == '__main__':
# 创建进程池
pool = Pool(2)
# 向进程池队列添加事件
for i in range(20):
msg = 'sunhao %d'%i
pool.apply_async(func=worker,args=(msg,))
# 关闭进程池
pool.close()
# 回收进程池
pool.join()
线程编程
线程的基本概念
- 什么是线程
【1】 线程被称为轻量级的进程
【2】 线程也可以使用计算机多核资源,是多任务编程方式
【3】 线程是系统分配内核的最小单元
【4】 线程可以理解为进程的分支任务 - 线程的特征
【1】 一个进程中可以包含多个线程
【2】 线程也是一个运行行为,消耗计算机资源
【3】 一个进程中的所有线程共享这个进程的资源
【4】 多个线程之间的运行互不影响各自运行
【5】 线程的创建和销毁消耗资源远小于进程
【6】 各个线程也有自己的ID等特征
treading模块创建线程
【1】 创建线程对象
from threading import Thread
t = Thread()
功能:创建线程对象
参数:target 绑定线程函数
args 元组 给线程函数位置传参
kwargs 字典 给线程函数键值传参
【2】 启动线程
t.start()
【3】 回收线程
t.join([timeout])
示例:各线程拥有相同的内存空间,所以共有的变量a只要一边改变了,另一边也改变。
from threading import *
from time import sleep
import os
a = 1
# 线程函数
def music():
global a
a = 10000
for i in range(3):
sleep(2)
print(os.getpid(),'播放:黄河大合唱')
# 创建线程对象
t = Thread(target=music)
# 开启线程
t.start()
for i in range(4):
sleep(1)
print(os.getpid(),'播放:葫芦娃')
print(a)
# 回收线程
t.join()
传参及如何批量创建线程
from threading import Thread
from time import sleep
def fun(sec,name):
print('线程函数参数')
sleep(sec)
print('%s执行完毕'%name)
jobs = []
# 循环创建线程
for i in range(5):
t = Thread(target=fun,args=(2,),kwargs={'name':'T%d'%i})
jobs.append(t)
t.start()
# 循环关闭线程
for i in jobs:
i.join()
线程对象的属性
t.name 线程名称
t.setName() 设置线程名称
t.getName() 获取线程名称
t.is_alive() 查看线程是否在生命周期
t.daemon 设置主线程和分支线程的退出关系
t.setDaemon() 设置daemon属性值
t.isDaemon() 查看daemon属性值
daemon为True时主线程退出分支线程也退出。要在start前设置,通常不和join一起使用。
线程池模块:threadpool
"""
thread_attr.py
线程属性示例
"""
from threading import Thread
from time import sleep
def fun():
sleep(3)
print("线程属性示例")
t = Thread(target = fun,name = "Tarena")
t.setDaemon(True) # 主线程退出分支线程也退出
t.start()
t.setName("Tedu")
print("Name:",t.getName()) # 线程名称
print("is alive:",t.is_alive()) # 是否在生命周期
print("Daemon:",t.isDaemon())
# t.join()
自定义线程类
- 创建步骤:①继承Thread,②重写__init__使用super加载父类属性,③重写run方法。
- 使用方法:①实例化对象,②调用start自动执行run,③调用join回收线程
线程的同步互斥
同步互斥的方法
- Event() wait()set() clear()
- Lock() acquire()release()
死锁:由于上锁造成的程序阻塞
Python线程GIL
-
Python线程的gil问题(全局解释器)
什么是GIL:由于Python解释器设计中加入了解释器锁,导致Python解释器同一时刻只能解释执行一个线程,大大降低了线程的执行效率。导致后果:因为遇到阻塞时线程会主动让出解释器,去解释其他线程。所以,Python多线程在执行多阻塞高延迟IO时可以提升程序效率,其他情况下并不能对效率提升。
GIL问题建议:
尽量使用进程完成无阻塞的并发行为;
不适用c作为解释器(java C#) -
结论
在无阻塞状态下,多线程程序和单线程程序执行效率几乎差不多,甚至还不如单线程效率。但是多进程运行相同内容却可以有明显的效率提升。
进程线程的区别
区别联系:
并发网络通信模型
- 常见模型分类
- 循环服务器模型:循环接收客户端请求,处理请求。同一时刻只能处理一个请求,处理完毕后再处理下一个。
优点:实现简单,占用资源少
缺点:无法同时处理多个客户端请求
适用情况:处理的任务可以快速完成,客户端无需长时间占用服务端程序。UDP比tcp更适合循环。 - IO并发模型:利用IO多路复用,异步IO等技术,同时处理多个客户端IO请求。
优点:资源消耗少,能同时高效处理多个IO行为
缺点:只能处理并发产生的IO事件,无法处理CPU计算
使用情况:HTTP请求,网络传输等都是IO行为。 - 多进程/线程网络并发模型:每当一个客户端连接服务器,就创建一个新的进程/线程为该客户端服务,客户端退出时再销毁该进程/线程。
优点:能同时满足多个客户端长期占有服务端请求,可以处理各种请求。
缺点:资源消耗较大
使用情况:客户端同时连接量较少,需要处理行为较复杂情况。
基于fork的多进程网络并发模型
'''
基于fork的多进程网络并发模型server
'''
import os,signal
from socket import *
# signal处理僵尸进程
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
# 创建TCP套接字
ADDR = ('0.0.0.0',8887)
sk = socket()
sk.bind(ADDR)
# 允许端口复用
sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
sk.listen()
# 循环创建子进程进行连接
while True:
conn, addr = sk.accept()
pid = os.fork()
if pid == 0:
sk.close()
while True:
try:
data = conn.recv(1024)
print(data.decode())
conn.send(b'OK')
except Exception as e:
print(e)
os._exit(0)
else:
conn.close()
sk.close()
练习:根据fork多进程并发网络模型思路,完成基于process的多进程并发网络模型
'''
基于processing的多进程并发网络模型
'''
from multiprocessing import Process
import os,signal
from socket import socket
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
ADDR = ('0.0.0.0',8887)
sk = socket()
sk.bind(ADDR)
sk.listen()
def connect_do(conn):
while True:
try:
data = conn.recv(1024)
print(data.decode())
conn.send(b'OK')
except Exception as e:
print(e)
os._exit(0)
while True:
conn, addr = sk.accept()
p = Process(target=connect_do,args=(conn,))
p.daemon = True
p.start()
基于threading的多线程网络并发
'''
基于threading的多线程并发网络模型
'''
from threading import Thread
import os,signal
from socket import socket
signal.signal(signal.SIGCHLD,signal.SIG_IGN)
ADDR = ('0.0.0.0',8885)
sk = socket()
sk.bind(ADDR)
sk.listen()
def connect_do(conn):
while True:
try:
data = conn.recv(1024)
print('recv',data.decode())
conn.send(b'OK')
except Exception as e:
print(e)
break
# os._exit(0)
while True:
conn, addr = sk.accept()
p = Thread(target=connect_do,args=(conn,))
p.daemon = True
p.start()
IO并发
- IO分类
阻塞IO,非阻塞IO,IO多路复用,异步IO等 - 阻塞IO
- 定义:在执行IO操作时如果执行条件不满足则阻塞。阻塞IO是IO的默认形态。
- 效率:阻塞IO是效率很低的一种IO。但是由于逻辑简单,所以是默认IO行为。
- 阻塞情况:
- 因为某种执行条件没有满足造成的函数阻塞:accept、input、recv—可以通过修改属性行为来完成
- 处理IO的时间较长产生的阻塞状态:网络传输,大文件读写—只能提高硬件
-
非阻塞IO
定义:通过修改IO属性行为,使原本阻塞的IO变为非阻塞的状态。- 设置套接字为非阻塞IO
sockfd.setblocking(bool)
功能:设置套接字为非阻塞IO
参数:默认为True,表示套接字IO阻塞;设置为False则套接字IO变为非阻塞 - 超时检测:设置一个最长阻塞时间,超过该时间后则不再阻塞等待。
sockfd.settimeout(sec)
功能:设置套接字的超时时间
参数:设置的时间
- 设置套接字为非阻塞IO
-
IO多路复用
- 定义
同时监控多个IO时间,当哪个IO时间准备就绪就执行哪个IO事件,以此形式可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高IO的执行效率。 - 具体方案:通过select模块实现
- select方法:Windows Linux Unix
- poll方法:linux Unix
- epoll方法:Linux
select模块
select方法(最多监控1024个)
- rs, 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_server_tcp服务
import select
from socket import socket
# 创建TCP套接字
sk = socket()
sk.bind(('0.0.0.0',8888))
sk.listen()
# 初始化rlist读监控事件列表
rlist = [sk,]
while True:
rs,ws,xs = select.select(rlist,[],[])
for i in rs:
if i == sk:
conn,addr = i.accept()
print(addr,'connected')
rlist.append(conn) # 将连接套接字加入到监听list中
else:
data = i.recv(1024)
print(data.decode())
try:
i.send(b'OK')
except Exception as e:
print(e)
i.close()
rlist.remove(i)# 将连接调节自从监听list中删除
位运算
* 运算符号(二进制)
* & 按位与---一0则0 (判别属性是否存在)
* | 按位或---一1则1 (增加属性)
* ^ 按位抑或---相同为0,不同为1
* << 向左移动低位补0
* >> 向右移动去掉低位
* 作用:属性中如果都是bool型的,那可以用二进制数字来代替这些属性,规定成一个属性。
poll方法(监控个数很多)
-
p = select.poll()
功能:创建poll对象
返回值:poll对象 -
p.register(fd,event)
- 功能:注册关注的IO事件
- 参数:
- fd 要关注的IO
- event 要关注的IO时间类型
- 常见类型:
- POLLIN(读IO事件rlist)
- POLLOUT(写IO事件wlist)
- ROLLERR(异常IOxlist)
- ROLLHUP(断开连接)
- eg. 用按位或连接多个类型:p.register(sockfd,POLLIN|POLLERR)
- 常见类型:
-
p.unregister(fd)
- 功能:
- 参数:fd可以是监控对象,也可以是fileno(文件描述符)
-
events = p.poll()
- 功能:阻塞等待监控的IO事件发生
- 返回值:返回发生的IO
- events格式 [(fileno,event),()…]
- 通过文件描述符找到对应的IO对象,建立fileno:IO对象的字典
poll_server代码:
from select import *
from socket import socket
# 创建TCP套接字
sk = socket()
sk.bind(('0.0.0.0',8888))
sk.listen()
# 创建poll对象
p = poll()
# 初始化fileno和对应套接字的字典
event_dict ={sk.fileno():sk}
# 将套接字加入到关注中
p.register(sk,POLLIN)
while True:
events = p.poll() #阻塞监控
# 循环取events中的fileno,从event_dict中获取对应的套接字
for f,e in events:
print(f,e)
print(event_dict[f])
# 判断套接字类型,并根据相关套接字类型做不同的操作。
if event_dict[f] == sk:
conn,addr = event_dict[f].accept()
p.register(conn, POLLIN)
event_dict[conn.fileno()] = conn
else:
data = event_dict[f].recv(1024)
print(data.decode())
try:
event_dict[f].send(b'OK')
except Exception as e:
print(e)
event_dict[f].close()
p.unregister(event_dict[f]) # 将断开的连接剔除出监听
epoll方法(逻辑上和poll一样,语法相似)
- 使用方法:基本与poll相同
- 生成对象改为epoll()
- 将所有时间类型改为EPOLL类型
- epoll特点
- epoll效率比select和poll高
- epoll监控IO数量比select多
- epoll的触发方式比poll要多(EPOLLET边缘触发)
epoll_server代码:
from select import *
from socket import socket
sk = socket()
sk.bind(('0.0.0.0', 8881))
sk.listen()
ep = epoll()
event_dict = {sk.fileno(): sk}
ep.register(sk, EPOLLIN)
while True:
events = ep.poll()
print(events)
for f, e in events:
print(f, e)
print(event_dict[f])
if event_dict[f] == sk:
conn, addr = event_dict[f].accept()
ep.register(conn, EPOLLIN)
event_dict[conn.fileno()] = conn
else:
data = event_dict[f].recv(1024)
print(data.decode())
try:
event_dict[f].send(b'OK')
except Exception as e:
print(e)
ep.unregister(event_dict[f])
作业:
- 重点代码自己能写;
- http1.0和http协议;
- 做一个复习计划,3周左右。函数编程、面向对象、闭包和装饰器、数据结构算法、进程线程网络
协程技术
基础概念
- 定义:纤程,微线程。是允许在不同入口点不同位置暂停或开始的计算机程序,简单来说,协程就是可以暂停执行的函数。
- 协程原理:记录一个函数的上下文,协程调度切换时会将记录的上下文保存,在切换回来时进行调取,恢复原有的执行内容,以便从上一次执行位置继续执行。
- 协程优缺点:
- 优点:
- 协程完成多任务占用计算机资源很少
- 由于协程的多任务切换在应用层完成,因此切换开销少
- 协程为单线程程序,无需进行共享资源同步互斥处理
- 缺点:
- 由于是个单线程,无法利用计算机多核资源
- 优点:
标准库协程实现
asyncio和async/await,由于生态不好,所以不常用
第三方协程模块
1.greenlet模块