python网络与并发编程总结
2020-12-24 19:36:31 21 举报
AI智能生成
python进阶
作者其他创作
大纲/内容
多进程(理论部分)
进程的基本概念
什么是程序?
程序是一堆代码文件。静态的
什么是进程?
一个正在被cpu运行的程序称为进程,动态的
串行、并行、并发及他们的效率
串行:当有多个进程需要被执行时,只有等执行完当前进程才会执行下一个进程
并行:同时执行多个进程,此时如果存在n个cpu(每个cpu是单核的情况下),那么如果有n个进程需要被执行时,n个cpu会同时执行这些进程
并发:其实是由CPU+多道技术实现的,CPU在执行一个进程时,如果遇到堵塞(IO),CPU会切换到另一个进程去执行
效率问题:并行的效率最高;并发与串行是要分情况去考虑的,如果此进程是IO密集型,那么并发的效率高,如果是计算密集型,那么串行效率高
进程的创建
计算机在开机过程中,操作系统会自动开启一些进程,如果想要创建新的进程,无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的(言外之意:一个进程的创建一定基于一个父进程);windows的系统调用位CreateProcess,linux的系统调用为fork。
进程的三个状态
运行、堵塞、就绪
多进程
multiprocess模块介绍
multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。
multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
Process类介绍
# 语法:Process([group [, target [, name [, args [, kwargs]]]]]) 创建一个子进程对象
注意:1.需要使用关键字来指定参数 2.args指定的target函数的位置参数是一个元组,必须有逗号
参数介绍
target表示调用对象,即子进程要执行的任务
args表示调用对象的位置参数元组,args=(1,2,'egon',)
kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}
name为子进程的名称
方法介绍
.start():启动进程,并调用该子进程中的p.run()
p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
p.is_alive():如果p仍然运行,返回True
p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间。
属性介绍
p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
p.name:进程的名称
p.pid:进程的pid
p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
注意:在windows中Process()必须放到 # if __name__ == '__mian__':下
于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。
如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。
这是隐藏对Process()内部调用的原,使用if __name__ == “__main __”,这个if语句中的语句将不会在导入时被调用。
如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。
这是隐藏对Process()内部调用的原,使用if __name__ == “__main __”,这个if语句中的语句将不会在导入时被调用。
创建开启子进程的两种方式
函数式创建
类创建
僵尸进程、孤儿进程、守护进程
僵尸进程:当子进程执行完毕后,会释放资源,只保留一些状态信息且等待父进程回收,这时进程被称为僵尸进程
孤儿进程:子进程执行过程中父进程由于某种原因(被杀死)无法对子进程回收,此时会将这个进程交由init进程来领养(回收),被领养的进程就被称为孤儿进程
守护进程:父进程结束,守护进程也随之结束
互斥锁(锁)
上一次锁,就需要解锁一次,保障数据不会错乱,串行的去执行
队列
实例化一个队列对象
Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
参数介绍
maxsize是队列中允许最大项数,省略则无大小限制。
方法介绍
q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
2 q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
复制代码
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
复制代码
生产者消费者模型三要素:
生产者,消费者,队列
多线程(理论部分)
进程线程分工
我们之前讲运行一个py文件,就是开启一个进程,在内存中开辟一个进程空间,将必要的数据加载到这个进程空间,然后cpu去调用这个进程的主线程去执行具体代码,一个进程里默认包含一个主线程
进程是资源单位,线程是最小的执行单位
举例
现实生活中,一个工厂可以看作为一个py文件,工厂中的多个车间可以看作多个进程,每个车间都有各自的工具(改锥等)相当于每个进程都有自己的独立空间,而车间中的流水线是真正生产东西的相当于线程(真正的执行者,每个进程默认都会有一个主线程)
进程线程对比
1、创建线程的开销少于创建进程的开销
2、开启线程的速度远高于开启进程的速度、创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用。
3、线程之间的数据是共享的,这样数据就不太安全,进程之间的数据隔离的
线程之间无父子之分
线程之间是没有主子、父子之分的,我们之所以称主线程、子线程仅仅是为了区分而已;进程之间是有父子、主子之分,子进程的开启依赖于父进程,子进程的所有的资源都是深copy父进程而且父进程对子进程进行回收。
主线程的结束
主线程需要等待主线程及其余线程结束之后再结束
解释:进程是资源单位,加载各种数据资源的,但是如果进程里面的线程执行完毕,结束了,进程就没有存在内存的必要了,但是主进程如果也结束了,其余线程没有数据资源了,只能强制的结束了,这样不合理。主线程代码执行完毕了之后,不能结束,他要等待其余线程结束了,他在结束,这样进程在结束。
创建线程的两种方式
函数式创建
面向对象式创建
线程的其他方法
t1 = Thread(target=task,args=(org1,))
t1.start() # 开启线程
t1.name # 获取当前线程的名字
t1.is_alive() # 返回布尔值,判断线程存活运行
threading.enumerate() # 返回一个列表,用于返回当前活跃的线程对象
threading.active_count() # 获取当前活跃线程的数量
t1.start() # 开启线程
t1.name # 获取当前线程的名字
t1.is_alive() # 返回布尔值,判断线程存活运行
threading.enumerate() # 返回一个列表,用于返回当前活跃的线程对象
threading.active_count() # 获取当前活跃线程的数量
守护线程
主线程和其余线程结束后,守护线程随之结束
与守护进程的区别
# 只要父进程结束守护进程就会随之结束
多线程
线程join
与进程join同理,使主线程堵塞,等待子线程结束之后主程序再执行
线程互斥锁(锁)
# 无论是进程锁还是线程锁,目的都是为了使程序串行处理,保障数据安全。
线程操作公共数据问题
由于线程之间数据共享,如果多个线程需要操作同一个数据,在执行其中一个线程时如果遇到IO,
那么CPU就会切换到其他线程去处理,导致其他线程所拿到的数据不是想要得到的,导致数据错乱;
此时我们就需要让多个线程串行起来,加锁处理即可
那么CPU就会切换到其他线程去处理,导致其他线程所拿到的数据不是想要得到的,导致数据错乱;
此时我们就需要让多个线程串行起来,加锁处理即可
具体使用
lock = Lock() # 实例化一个锁对象
lock.acquire() # 上锁,必须释放才可再次上锁
lock.release() # 释放这把锁
lock.acquire() # 上锁,必须释放才可再次上锁
lock.release() # 释放这把锁
死锁现象
虽然锁可以保证数据安全,但锁如果设置的多的画,可能会导致死锁现象;进程锁同理
当有需要需要上两把锁时使用递归锁解决
递归锁
递归锁也是一把锁,它可以上锁多次,也可以释放多次,引用了计数功能
锁一次+1一次,释放一次-1一次,在计数器不为0时其他线程或进程不允
许抢夺这把锁,直至为0时即可
锁一次+1一次,释放一次-1一次,在计数器不为0时其他线程或进程不允
许抢夺这把锁,直至为0时即可
使用
lock = RLock() # 实例化一个递归锁对象
lock.acquire() # 上锁,必须释放才可再次上锁
lock.release() # 释放这把锁
lock.acquire() # 上锁,必须释放才可再次上锁
lock.release() # 释放这把锁
信号量
信号量其实也是一把锁,不过它是一个可以设置数量的,当你设置了值后就允许这个值的进程(线程)
进入,当有一个或多个释放掉后,才允许一个或多个进入
进入,当有一个或多个释放掉后,才允许一个或多个进入
GIL全局锁
# 理论上多核CPU在处理多线程时是可以并形处理,但由于GIL全局锁,他对线程在进入Cpython解释器前
加了一把锁(类似与互斥锁),使得多线程无法多核处理,但可以并发处理,保障Cpython解释器的数据安全
加了一把锁(类似与互斥锁),使得多线程无法多核处理,但可以并发处理,保障Cpython解释器的数据安全
为什么加这把锁?
在单进程多线程并行或并发情况下保障Cpython解释器数据安全不会错乱;
如果主动在源码当中加入互斥锁也可以保障解释器数据,但这则属要加入
大量的互斥锁,这就大大影响了开发效率
如果主动在源码当中加入互斥锁也可以保障解释器数据,但这则属要加入
大量的互斥锁,这就大大影响了开发效率
为什么不去掉这把锁
Cpython设计之初是围绕单线程设计的,如果想要去掉的话,
无异于重构Cpython,耗时耗力。
无异于重构Cpython,耗时耗力。
解决这把锁问题的处理方案
# 随着python版本的不断更新,不断优化;处理多线程并发的效率得以提高,与多进程单线程并行的效率相差不断减小
1、可以使用多进程单线程并行代替,只不过开启进程的开销较大,但执行效率相差较小
2、可以采用C的模块或者嵌入C语言去处理这种单进程的多线程的并行的问题。
针对IO密集和计算密集型的数据解决方案
IO密集型:我们后续面对的业务,大部分都是IO密集型的,
遇到这种情况使用**单进程多线程并发处理是最好的解决方案**
遇到这种情况使用**单进程多线程并发处理是最好的解决方案**
计算密集型:多个任务都是纯计算(导弹轨迹计算)而没有IO阻塞,
那么此时应该利用**多进程并行**的处理任务。
那么此时应该利用**多进程并行**的处理任务。
GIL的优缺点
优点:便于Cpython解释器内部资源管理,保障数据安全
缺点:单进程多线程不能多核处理
GIL全局解释器锁并不是让Cpython不能利用多核,
多进程是可以利用多核的,况且IO密集型的任务,
单进程多线程处理即可
多进程是可以利用多核的,况且IO密集型的任务,
单进程多线程处理即可
GIL全局锁与互斥锁的区别
两者虽然性质相同,但GIL全局锁是保障Cpython解释器及各种库数据的安全,而互斥锁保障程序数据的安全
进程池 and 线程池
# 在C/S架构中,如果客户端请求一个任务服务端就开启一个进程或线程为其执行的话;
如果这个请求量过大,远远超出了服务器的负载范围那么将导致服务器瘫痪;此时我们
针对这类问题可以使用进程池或线程池来解决,控制(设置)创建线程或进程的数量,
保障同一时刻至多创建的数量
如果这个请求量过大,远远超出了服务器的负载范围那么将导致服务器瘫痪;此时我们
针对这类问题可以使用进程池或线程池来解决,控制(设置)创建线程或进程的数量,
保障同一时刻至多创建的数量
线程池
from concurrent.futures import ThreadPoolExecutor # 导入模块
thread_poor = ThreadPoolExecutor(num) # 实例化一个线程池对象,num设置数量
thread_poor.submit(task,org) # 发布任务,task任务(函数),org参数
thread_poor = ThreadPoolExecutor(num) # 实例化一个线程池对象,num设置数量
thread_poor.submit(task,org) # 发布任务,task任务(函数),org参数
进程池
from concurrent.futures import ThreadPoolExecutor # 导入模块
thread_poor = ThreadPoolExecutor(num) # 实例化一个线程池对象,num设置数量
thread_poor.submit(task,org) # 发布任务,task任务(函数),org参数
thread_poor = ThreadPoolExecutor(num) # 实例化一个线程池对象,num设置数量
thread_poor.submit(task,org) # 发布任务,task任务(函数),org参数
阻塞、非堵塞、同步、异步
堵塞
在程序执行过程中,遇到了IO(网络请求、文件数据库的操作),
程序就停住了,此时程序就暂时挂起,cpu被操作系统从此程
序中切走,等到IO完毕,操作系统再将cpu切回来。
程序就停住了,此时程序就暂时挂起,cpu被操作系统从此程
序中切走,等到IO完毕,操作系统再将cpu切回来。
非堵塞
在程序执行中,没有IO(全部都是计算、运算)或者(程序遇到了IO时,
通过程序的某些操作将cpu切换到程序的另一些正在运行的进程中,
程序是一直运行的。协程)
通过程序的某些操作将cpu切换到程序的另一些正在运行的进程中,
程序是一直运行的。协程)
同步
发布第一个任务,然后等待,等待第一个任务的返回结果之后,在发布第二个任务。
异步
一次性将所有的任务全部发布,继续向下执行。
同步与串行的区别?以及异步和并发的区别?
角度不同,同步和异步是针对发布任务的角度,而串行和并发是针对程序的角度
同步调用
thread_poor = ThreadPoolExecutor(max_workers=5) # 实例化一个线程池对象
# 发布20个任务
for i in range(20):
obj = thread_poor.submit(task,i) # 返回的是任务当前的状态对象
print(obj.result())
thread_poor.shutdown()
# shutdown的用法
# 1. 让主进程/主线程阻塞,等进程池线程池内所有的进程线程将所有的任务执行完毕之后,主进程线程在执行。
# 2. 在shutdown之后,不允许在给线程池/进程池 发布新的任务
# 发布20个任务
for i in range(20):
obj = thread_poor.submit(task,i) # 返回的是任务当前的状态对象
print(obj.result())
thread_poor.shutdown()
# shutdown的用法
# 1. 让主进程/主线程阻塞,等进程池线程池内所有的进程线程将所有的任务执行完毕之后,主进程线程在执行。
# 2. 在shutdown之后,不允许在给线程池/进程池 发布新的任务
异步调用
process_poor = ProcessPoolExecutor(max_workers=5)
# 一次性发布20个任务。
for i in range(20):
process_poor.submit(task, i)
# 一次性发布20个任务。
for i in range(20):
process_poor.submit(task, i)
异步调用+回调函数
引子(浏览器、爬虫原理)
浏览器原理
浏览器会将你的请求数据通过网络发送到百度的服务器,服务器接收到请求数据,
验证请求数据之后,返回给浏览器软件一个html页面数据,浏览器接收到这个
html页面数据通过浏览器的内核的渲染机制,渲染成美丽的页面。
验证请求数据之后,返回给浏览器软件一个html页面数据,浏览器接收到这个
html页面数据通过浏览器的内核的渲染机制,渲染成美丽的页面。
爬虫原理
爬虫是模拟一个浏览器向服务器请求数据
数据请求成功之后,通过数据清洗获取目标数据
import requests
ret = requests.get('http://www.baidu.com')
if ret.status_code == 200:
print(ret.text)
ret = requests.get('http://www.baidu.com')
if ret.status_code == 200:
print(ret.text)
子主题
线程队列
import queue
q = queue.Queue(num) # 实例对象
q.put() # 向队列里添加值
q.get() # 从队列中获取值
q = queue.Queue(num) # 实例对象
q.put() # 向队列里添加值
q.get() # 从队列中获取值
先进先出队列
后进先出队列
优先级队列
事件Event
当有两个进程或者两个线程,其中一个线程或者进程需要根据另一个进程或线程来判断是是否往下运行时,此时用event即可搞定
event = Event() # 实例化一个事件对象
# 在一个进程中设置
event.set()
# 在另一个进程或线程中设置
event.wait(timeout)
执行到wait时程序会阻塞,只有当另一个进程执行到set后,才可继续向下执行
# 在一个进程中设置
event.set()
# 在另一个进程或线程中设置
event.wait(timeout)
执行到wait时程序会阻塞,只有当另一个进程执行到set后,才可继续向下执行
操作系统及操作系统的发展史
什么是操作系统?
操作系统是一个大的软件,用来协调管理控制计算机的硬件与软件的
操作系统的作用
将硬件的一些复杂的调用封装成简单的接口
将多个进程对cpu的资源竞争变得合理,有序
操作系统的发展史
第一代(1940~1955)晶体管——穿孔卡片
第二代(1955~1965)磁带存储——批处理系统
第三代(1955~1965)集成电路(现在服务器的前身),多道系统
# 多道技术:当有多个进程需要被执行,如果一个进程在执行过程中遇到堵塞(IO)cpu会切换到另一个进程执行实现并发
第四代(1980至今)现代计算机
osi五层模型
物理层
一堆物理连接介质
数据链路层
给数据封装mac地址
mac地址是计算机唯一的物理地址
对应的物理设备
交换机
交换机的学习功能:交换机可以对一组数据进行拆包,封包。如果连接交换机的一个计算机发出一组数据,交换机可以拆包记录次端口对应的该计算机的mac地址。所以就形成了一个端口号与mac地址的对应表。交换机在接收到一组数据之后,先查看交换机保存的对应表,如果能够查到目标mac地址对应的端口号,直接单播发送,否则广播发送。
协议
ARP协议:通过对方ip地址获取对方mac地址
网络层
通过ip地址与子网掩码可以确认目标计算机与原计算机是否同在一个局域网
传输层
端口协议:UDP TCP协议
0~65535端口号,0~1024系统占用
应用层
功能
确定客户进程和服务器进程的端口号
确定客户进程和服务器进程的IP地址
选择一个合适的应用层协议
协议
DNS、FTP、HTTP、telnet、HTTP、SMTP、POP3、SNMP、FTP
端口协议:TCP、UDP协议
总结:TCP的优点其实就是UDP的缺点,TCP是面向连接流式协议,而UDP是面向数据报协议
安全:UDP相对不可靠,而TCP相对可靠
速度:UDP传输速度相对快,而TCP相对较慢
有无堵塞控制:TCP有堵塞控制,而UDP没有
TCP的三次握手四次挥手
三次握手
第一次握手:发送方向接受方发送请求连接的消息,标志控制位SYN为1
第二次握手:接受方收到发送来的消息后确认,标志控制位ACK为1,然后接受方也要向发送方发送请求连接的消息此过程与第一次握手相同
第三次握手:发送方收到消息后回给接受方确认消息,此过程与第二次握手确认消息相同
四次挥手
第一次断开:发送方向接受方请求断开连接,标志控制位FIN为1
第二次断开:接受方收到消息后确认,标志控制为ACK为1
第三次断开:接受方向发送方发送请求断开连接的请求,此过程与第一次断开相同
第四次断开:发送方收到消息后确认消息,此过程与第二次断开相同
相关面试题
为什么TCP断开连接需要四次挥手?
如果已经建立了连接,但是客户端突然故障了怎么办?
什么是SYN洪水攻击?
socket
什么是socket
Socket又称为套接字,他是应用层与传输层的中间抽象层,它是一组接口,在设计模式中,Socket其实就是一个门面模式,将原本要与底层硬件和操作系统相关联的复杂功能逻辑给你封装成一个个简单的接口(类似与函数的调用)直接使用
Socket就是一种模块,通过模块的一些简单方法去使用
socket缓冲区
为什么设置socket缓冲区?
可以让数据暂存,防止数据丢失
收发数据流畅不间断、稳定、提升收发数据的效率
详解
每个socket创建被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write/send并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情,read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。
recv工作原理
当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。
关闭远程端并读取所有数据后,返回空字符串。
粘包
什么是粘包?
指TCP协议中,发送方若干数据到接收方接收时粘成一包
什么情况出现粘包?
发送方向接收方发送数据,到达接收方缓冲区中时接收方recv的字节数小于缓冲区中字节的数量,此时recv只能接收一小部分,服务端下次再接收时还是从缓冲区拿上次遗留的数据,产生粘包
连续短暂的发送少量数据,接收时数据可能会粘在一起。
粘包的解决方案
利用struct固定头部
固定头部+json字典实现文件上传与下载
收藏
收藏
0 条评论
下一页