并发编程
学习并发的目的
我们之前写的程序代码都是从上往下运行,如果中间某个函数卡住,程序就会等待,不继续运行了,这个时候我们就要编写一种可以同时执行多个任务的程序来提高效率,这就是我们要学习并发的目的,串行和并发都是程序处理任务的方式,总的来说就是如何让程序提高效率
并发与串行
仔细说一下这个并发与串行,上边说了程序默认执行的方式就是串行,即程序自上而下执行,必须要把当前任务执行完毕才能执行下一个任务,无论当前任务需要多长时间,
比如以下问题:
- tcp服务器的服务器中,如果正在进行通讯循环则无法处理其他的客户端请求
- 如从硬盘读取非常大的文件时
- 执行了input
实现并发的方式
- 多进程
- 多线程
- 协程
进程是什么
概念:进程指的是正在运行程序是操作系统调度以及进行资源分配的基本单位,是一个资源单位
-
进程是怎么来的
当一个程序从硬盘读入到内存时,进程就产生了
-
多进程
指的是同一时间有多个程序被装入内存并执行,进程来自于操作系统进行调度以及资源分配,多进程的实现原理其实就是操作系统调度进程的原理
操作系统是什么
理解:操作系统是一款特殊的软件,
操作系统的主要功能:
- 隐藏了硬件系统复杂的操作,提供了简单直观的API接口
- 将硬件的竞争变得有序可控
- 提供了GUI图形化用户界面
操作系统与普通软件的区别:
- 操作系统可以直接与硬件交互
- 操作系统是受保护的,不能直接被修改的程序
- 操作系统更加长寿,一旦完成基本不会修改,例如系统内核
操作系统的发展史
-
第二代计算机使用的批处理系统
缺点:
- 需要人为参与
- 任务串行执行
- 程序员调试效率低
-
第三代计算机
- 使用SPOOLING联机技术
- 多道技术(重点,后边有说)
- 多终端多用户
这里重点学习多道技术
实现原理:
- 空间复用
统一时间,加载多个任务到内存中,多个进程之间内存区域需要相互隔离,这种隔离是物理层面的隔离,其目的是为了保证数安全
- 时间复用
指的是,操作系统会在多个进程之间做切换执行
切换任务的两种情况
1.当一个进程遇到了IO操作 时会自动切换
2.当一个任务执行时间超过阈值会强制切换
注意:在切换前必须保存状态,以便后续恢复执行
并且 频繁的切换其实也需要消耗资源
当所有任务都没有IO操作时,切换执行效率反而降低,但是为了保证并发执行 必须牺牲效率
简单的总结这个时间 复用就是切换加保存
有了多道技术,计算机就可以同时并发的处理多个任务
-
第四代计算机
大规模继承电路+多用户多终端系统
体积降低,成本降低,发展处个人计算机(PC)
特点是:大多具备GUI界面,即使是普通人不具备专业及技能也能流畅使用
并发编程中的重要概念(必须掌握)
串行:自上而下顺序执行
并发:多个任务同时执行,但是本质是在不同进程间切换执行,由于速度非常快所以感觉是同时运行
并行:这个才是真正的同时运行,必须具备多核CPU,有几个核心就能并行几个任务,当任务数量超过核心数还是并发执行
故,以上三个概念都是用于描述处理任务的方式
阻塞和非阻塞
概念:
-
阻塞:指的是程序遇到了IO操作,无法继续执行代码时的一种状态
-
非阻塞:指的是程序没有遇到IO操作的一种状态
阻塞和非阻塞也可用来描述执行任务的方式
input 默认是是一个阻塞操作
我们可以用一些手段将阻塞的操作变成非阻塞的操作 , 例如非阻塞的socket
-
一个进程的三种状态:
一: 阻塞
二:运行
三:就绪
进程的创建与销毁(了解)
对于通用计算机而言,必须具备创建和销毁进程的能力
创建
- 用户的交互式请求,如鼠标双击
- 由一个正在运行的程序调用了开启进程的接口 例如:subprocess
- 一个批处理作业开始
- 系统初始化
销毁
- 任务完成
- 异常退出
- 严重错误
- 强制关闭
进程和程序
程序是一堆代码放在一个文件中,通常后缀为exe,原本是存储在硬盘上的
进程是将代码从硬盘读到内存然后执行,产生的
进程是由程序产生的
一个程序可以产生多个进程,例如qq多开 每一个进程都具备一个PID 进程变编号 且是唯一的
进程的层次结构(了解)
在linux中 进程具备父子关系,是一个树状结构 ,可以互相查找到对方
在windows 没有层级关系 , 父进程可以将子进程的句柄转让
父进程 子进程? 例如qq开启了浏览器 那么qq是父进程 浏览器是子进程
PID与PPID
PID 是当前进程的编号
PPID 是父进程的编号
注意:当我们运行py文件时其实运行的是python解释器
访问PID与PPID
import os
os.getpid()
os.getppid()
python如何使用多进程
创建子进程的方式
1.导入multiprocessing 中的Process类 实例化这个类 指定要执行的任务 target
import os
from multiprocessing import Process
"""
Process 就表示进程
为什么要开进程
"""
def task():
print("this is sub process")
print("sub process id %s" % os.getpid())
if __name__ == '__main__':
# ######注意 开启进程的代码必须放在 ————main————判断下面
# 实例化一个进程对象 并制定他要做的事情 用函数来指定
p = Process(target=task)
p.start() # 给操作系统发送消息 让它开启进程
print("this is parent process")
print("parent process is: %s" % os.getpid())
print("over")
linux 与windows开启进程的方式不同
linux 会将父进程的内存数据 完整copy一份给子进程
注意:
windows 会导入父进程的代码 从头执行一遍 来获取需要处理的任务
所以在编写代码时如果是windows一定要将开启进程的代码放main判断中
linux 可以不放
2.导入multiprocessing 中的Process类 继承这个类 覆盖run方法 将要执行的任务放入run中开启进程时会自动执行该函数
from multiprocessing import Process
import os
class Downloader(Process):
# def __init__(self,url,size,name):
# super().__init__()
# self.url = url
# self.size = size
# self.name = name
def run(self):
print(os.getpid())
pass
if __name__ == '__main__':
m = Downloader()
m.start()
print("parent over",os.getpid())
如果需要对进程对象进行高度自定义那就可以继承它
进程之间内存相互隔离
from multiprocessing import Process
import os,time
a = 257
def task():
global a
# print("2",a,id(a))
a = 200
if __name__ == '__main__':
p = Process(target=task)
p.start() # 向操作系统发送指令
time.sleep(4)
print(a)
join函数
from multiprocessing import Process
import time
def task1(name):
for i in range(10000):
print("%s run" % name)
def task2(name):
for i in range(100):
print("%s run" % name)
if __name__ == '__main__': # args 是给子进程传递的参数 必须是元组
p1 = Process(target=task1,args=("p1",))
p1.start() # 向操作系统发送指令
# p1.join() # 让主进程 等待子进程执行完毕在继续执行
p2 = Process(target=task2,args=("p2",))
p2.start() # 向操作系统发送指令
p2.join() # 让主进程 等待子进程执行完毕在继续执行
p1.join()
#需要达到的效果是 必须保证两个子进程是并发执行的 并且 over一定是在所有任务执行完毕后执行
print("over")
案例:
# join的使用
from multiprocessing import Process
import time
def task1(name):
for i in range(10):
print("%s run" % name)
if __name__ == '__main__': # args 是给子进程传递的参数 必须是元组
ps = []
for i in range(10):
p = Process(target=task1,args=(i,))
p.start()
ps.append(p)
# 挨个join以下
for i in ps:
i.join()
print("over")
进程对象的常用属性
if __name__ == '__main__':
p = Process(target=task,name="老司机进程")
p.start()
# p.join()
# print(p.name)
# p.daemon #守护进程
# p.join()
# print(p.exitcode) # 获取进程的退出码 就是exit()函数中传入的值
# print(p.is_alive()) # 查看进程是否存活
# print("zi",p.pid) # 获取进程id
# print(os.getpid())
# p.terminate() #终止进程 与strat 相同的是 不会立即终止,因为操作系统有很多事情要做
# print(p.is_alive())
僵尸进程与孤儿进程
孤儿进程 当父进程已经结束 而子进程还在运行 子进程就称为孤儿进程 尤其存在的必要性,没有不良影响
僵尸进程 当一个进程已经结束了但是,它仍然还有一些数据存在 此时称之为僵尸进程
在linux中,有这么一个机制,父进程无论什么时候都可以获取到子进程的的 一些数据
子进程 任务执行完毕后,确实结束了但是仍然保留一些数据 目的是为了让父进程能够获取这些信息
linux中 可以调用waitpid来是彻底清除子进程的残留信息
python中 已经封装了处理僵尸进程的操作 ,无需关心