一、进程
进程:资源分配的最小单位(QQ、微信)
多进程:程序自动创建主进程;自己创建子进程
1. 导入进程模块
import multiprocessing
2. 创建进程对象
coding_process = multiprocessing.Process(target=coding) # target=函数名 music_process = multiprocessing.Process(target=music)
3. 启动进程
coding_process.start() music_process.start()
对比:
1. 主进程
import time def coding(): for i in range(3): print("coding...") time.sleep(0.2) def music(): for i in range(3): print("music...") time.sleep(0.2)coding()
musci()
结果:
2. 多进程
# 代码只能作为脚本直接执行,import 到其他脚本中是不会被执行的 if __name__ == '__main__': # 创建进程对象 coding_process = multiprocessing.Process(target=coding) # target=函数名 music_process = multiprocessing.Process(target=music) # 启动进程 coding_process.start() music_process.start()
结果:
二、进程(参数)
args:以元组方式给执行任务传参 args=(a, b) #a、b为两个参数
kwargs: 以字典方式给执行任务传参 kwargs={} #
三、进程编号
os.getppid() 获取当前进程编号
os.getppid() 获取当前父进程编号
import os
def work(): # 定义函数
print("work进程编号:", os.getpid())
print("work父进程编号:", os.getppid())
def coding(): print("coding %d" % os.getpid()) print("coding %d" % os.getppid()) # 父进程 for i in range(3): print("coding...") time.sleep(0.2) def music(): print("music %d" % os.getpid()) print("music %d" % os.getppid() # 父进程 for i in range(3): print("music...") time.sleep(0.2)
if __name__ == '__main__': #主进程 print("music %d" % os.getpid()) print("music %d" % os.getppid()) # 创建进程对象 coding_process = multiprocessing.Process(target=coding) # target=函数名 music_process = multiprocessing.Process(target=music) #2为实参 # 启动进程 coding_process.start() music_process.start()说明:coding和music是由主进程创建的子进程
四、 进程间不共享全局变量
五、主进程、子进程结束顺序
主进程会等所有子进程执行完毕后再执行
守护子进程:主进程执行完毕后不管子进程是否执行完毕都会销毁。
work_process.daemon = True
手动销毁子进程: work_process.terminate()
if __name__ == "__main__": # 创建子进程 work_process = multiprocessing.Process(target=work) 1) # 设置守护主进程 work_process.daemon = True # 启动子进程 work_process.start() # 延时1秒 time.sleep(1) 2) # 手动结束 work_process.terminate() print("主进程执行完毕")
六、线程
多线程是实现多任务的一种方式
程序执行的最小单位,一个进程里最少有一个进程来执行
同一进程的多个线程共享所拥有的的全部资源
线程创建步骤:
1. 导入线程模块
import threading
2. 通过线程类创建线程对象
线程对象=threading.Thread(target=任务名) # 函数名
3. 启动线程执行任务
线程对象.start()
1. 线程(同进程)
dance = threading.Thread(target=sing, kwargs={"count": 5})
sing = threading.Thread(target=sing, arg(5, ))
线程执行无序
import threading import time def task(): time.sleep(1) # threading.current_thread() 当前线程 .name 获取线程名字 print(f"当前的线程是 {threading.current_thread().name}") if __name__ == '__main__': print(f"当前的主线程: {threading.current_thread().name}") # 多个线程 for _ in range(5): # _也是变量 # 创建子线程 sub_thread = threading.Thread(target=task) sub_thread.start()
结果
2. 主线程会等待所有子线程运行结束后再执行
3. 线程之前共享全局变量
1)资源竞争:当多个线程调用一个全局变量资源时,全局变量资源不断的被修改,调用过程存在资源竞争
解决方法: 采用全局变量锁,每次线程调用后,将该资源上锁,不允许再被调用,只有调用结束后打开锁,保证全局变量资源的安全。
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
注意:如果aquire调用前已有互斥锁,则会堵塞,等待解锁后,才会调用
import threading
# 定义全局变量
g_num = 0
# 创建互斥锁
lock = threading.Lock()
# 循环一次给全局变量加1
def sum_1():
# 上锁
lock.aquire()
for i in range(1000000):
global g_num # 全局变量
g_num += 1
print("sum1:", g_num)
#释放锁
lock.release()
# 循环一次给全局变量加1
def sum_2():
# 上锁
lock.aquire()
for i in range(1000000):
global g_num # 全局变量
g_num += 1
print("sum2:", g_num)
#释放锁
lock.release()
if __name__ == '__main__':
# 创建两个线程
first_thread = threading.Thread(target=sum_num1)
second_thread = threading.Thread(target=sum_num2)
# 启动线程
first_thread.start()
second_thread.start()
总结:
1. 互斥锁可以保证多个线程访问同一个全局变量时不会出现错误
2. 加上互斥锁后,多任务变为单任务,性能下降
3. 互斥锁没使用好容易出现死锁情况
2)死锁
一直等待对方释放锁的情景就是死锁
死锁会造成程序无法响应
例如:线程A的互斥锁未释放就让线程B的互斥锁上锁,等待资源释放
解决方案:
- 程序设计时要尽量避免(银行家算法)
- 添加超时时间等
银行家算法的核心思想:每次资源分配前判断此次分配是否会导致系统进入不安全状态,以此来决定是否答应资源分配请求
总结
关系:
1. 线程是依附在线程里的,没有进程就没有线程
2. 一个进程默认提供一条线程,进程可以创建多个线程
对比:
1. 进程之间不共享全局变量
2. 线程之间共享全局变量,但注意资源竞争问题,解决方法是互斥锁,但要注意死锁
3. 创建线程资源开销比创建线程资源开销大
4. 进程是操作系统分配资源的基本单位,线程是CPU调度的基本单位
优缺:
进程:多核,资源开销大
线程:不能多核,资源开销小
七、进程之间的通信
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的, 所以进程之间要通信必须通过内核。
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信机制。
主要的过程如下图所示(来自知乎):
信号量:信号量的工作机制(因为真的很简单),可以直接理解成计数器(当然其实加锁的时候肯定不能这么简单,不只只是信号量了),信号量会有初值(>0),每当有进程申请使用信号量,通过一个P操作来对信号量进行-1操作,当计数器减到0的时候就说明没有资源了,其他进程要想访问就必须等待(具体怎么等还有说法,比如忙等待或者睡眠),当该进程执行完这段工作(我们称之为临界区)之后,就会执行V操作来对信号量进行+1操作