day06进程与线程
一. 进程
每个软件都可以看作是一个进程(数据隔离)
软件内的多个任务可以看作是多个线程(数据共享)
单核CPU: 宏观并行, 微观并发
真正的并行必须有多核CPU
多任务介绍
概述
多任务指的是, 多个任务"同时"执行
目的
节约资源, 充分利用CPU资源, 提高效率
表现形式
并发
针对于单核CPU来讲, 如果有多个任务同时请求执行, 但是同一瞬间CPU只能执行1个(任务), 于是就安排他们交替执行.
因为时间间隔非常短, 所以宏观上看是并行, 但是微观上还是并发的.
并行
针对多核CPU来讲, 多个任务可以同时执行
进程介绍
概述
进程: 指的是可执行程序(*.exe), 也是CPU分配资源的最小单位.
线程: 进程的执行路径, 执行单元, 也是CPU调度资源的最小单位
解释:
进程: 车
线程: 车道
多进程实现
步骤
-
导包(multiprocessing)
-
创建进程对象, 关联 该进程要执行的任务(函数)
-
启动进程执行任务
代码
import multiprocessing, time # 定义代码函数 def coding(): for i in range(1, 21): time.sleep(1) print(f'正在敲代码---- {i}') # 定义音乐函数 def music(): for i in range(1, 21): time.sleep(1) print(f'正在听音乐**** {i}') if __name__ == '__main__': # 创建进程对象 p1 = multiprocessing.Process(target=coding) p2 = multiprocessing.Process(target=music) # 执行进程 p1.start() p2.start()
进程参数
参数
target: 用于关联 进程要执行的任务的.name: 进程名, 默认是: Process-1, Process-2,...., 可以手动修改, 一 般不改.args: 可以通过 元组 的形式传递参数, 实参的个数 及 对应的数据类型 要和 形参的个数及类型 一致.kwargs: 可以通过 字典 的形式传递参数, 实参的个数 要和 形参的个数 一致.
代码演示
import multiprocessing, time def coding(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在敲第{i}行代码-') def music(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在听第{i}首音乐********') if __name__ == '__main__': p1 = multiprocessing.Process(target=coding, args=('小明', 21)) p2 = multiprocessing.Process(target=music, kwargs={'num': 21, 'name': '小明'}) p11 = multiprocessing.Process(target=coding, args=('小明', 21), name='QQ') p22 = multiprocessing.Process(target=music, kwargs={'num': 21, 'name': '小明'}, name='WX') print(f'p1:{p1.name}') print(f'p2:{p2.name}') print(f'p11:{p11.name}') print(f'p22:{p22.name}') p1.start() p2.start()
main进程
解释
main程序入口也相当于一个进程, 在程序执行时遇到自定义进程会发生资源抢占, 上述代码中在输出进程对象后, 进程才启动, 所以上述的自定义进程不会和main进程强制资源, 并且自定义进程的启动需要一定时间, 此时的main进程可能已经完成自己的任务, 执行自定义进程.
图解
代码
import multiprocessing, time def coding(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在敲第{i}行代码-') def music(name, num): for i in range(1, num): time.sleep(0.01) print(f'{name}正在听第{i}首音乐********') if __name__ == '__main__': p1 = multiprocessing.Process(target=coding, args=('小明', 11)) p2 = multiprocessing.Process(target=music, kwargs={'num': 11, 'name': '小明'}) # p11 = multiprocessing.Process(target=coding, args=('小明', 11), name='QQ') # p22 = multiprocessing.Process(target=music, kwargs={'num': 11, 'name': '小明'}, name='WX') p1.start() p2.start() # print(f'p1:{p1.name}') # time.sleep(0.1) # print(f'p2:{p2.name}') # time.sleep(0.1) # print(f'p11:{p11.name}') # time.sleep(0.1) # print(f'p22:{p22.name}') for i in range(1, 21): print(f'main: ------- {i}') time.sleep(0.1)
注: 上述代码中, 若将main中的循环放到自定义进程前, 则自定义进程不会和main抢占资源, 因为执行完main中的循环才执行到自定义进程, 可以把main函数看作栈, 从上到下执行
代码执行结果(每次可能都不一样)
进程编号
前面创建的进程为main进程的子进程, main为前面进程的父进程,
main的父进程为pycharm
程序执行默认有main函数
在操作系统中, 每个进程都有自己唯一的ID, 且当前进程被终止时, 该ID会被回收, 即: ID是可以重复使用的.
目的
-
查找子进程是由那个父进程创建的, 即: 找子进程和父进程的ID.
-
方便我们维护进程, 例如: kill -9 pid值 可以强制杀死进程.
当前编号
方式1: os模块的 getpid()方法方式2: multiprocessing模块的current_process()方法的 pid属性
当前的父进程编号
os模块的 getppid()方法 parent process: 父进程
代码演示
# 导包 import multiprocessing, time, os # 案例: 小明一边敲着第n行代码, 一边听着第n首音乐. # 1. 定义函数, 表示: 敲代码. def code(name, num): for i in range(1, num + 1): print(f'{name} 正在敲第 {i} 行代码...') time.sleep(0.1) # multiprocessing模块的current_process()函数: 获取当前进程对象 print(f'当前进程(p1)的id: {os.getpid()}, {multiprocessing.current_process().pid}, 父进程的id为: {os.getppid()}') # 2. 定义函数, 表示: 听音乐 def music(name, count): for i in range(1, count + 1): print(f'{name} 正在听第 {i} 首音乐.........') time.sleep(0.1) print(f'当前进程(p2)的id: {os.getpid()}, {multiprocessing.current_process().pid}, 父进程的id为: {os.getppid()}') # 在main中测试. if __name__ == '__main__': # 3. 创建进程对象. # Process类的参数: target: 关联的函数名, name: 当前进程的名字, args:元组的形式传参. kwargs: 字典的形式传参. p1 = multiprocessing.Process(name='Process_QQ', target=code, args=('乔峰', 10)) p2 = multiprocessing.Process(name='Process_Wechat', target=music, kwargs={'count': 10, 'name': '虚竹'}) # print(f"p1进程的名字: {p1.name}") # print(f"p2进程的名字: {p2.name}") # 4. 启动进程. p1.start() p2.start() print(f'当前进程(main)的id: {os.getpid()}, {multiprocessing.current_process().pid}, 父进程的id为: {os.getppid()}')
执行效果
进程注意事项
关于进程, 你要记忆的内容: 1. 进程之间数据是相互隔离的. 例如: 微信(进程) 和 QQ(进程)之间, 数据就是相互隔离的.
默认情况下, 主进程会等待它所有的子进程 执行结束再结束.
细节: 多进程之间, 针对于 main进程的(外部资源), 每个子进程都会拷贝一份, 进行执行.
数据隔离
代码
import multiprocessing, time # 需求: 定义1个列表, 然后定义两个函数, 分别往列表中添加数据, 获取数据. # 之后用两个进程关联这个两个函数, 启动进程并观察结果. # 1. 定义全局变量 my_list my_list = [] print('我是main外资源, 看看我执行了几遍!') # 执行三次 # 2. 定义函数, 实现往列表中添加数据. def write_data(): for i in range(1, 6): # 具体的添加元素的动作 my_list.append(i) # 打印添加动作. print(f'添加 {i} 成功!') # 细节: 添加数据完毕后, 打印结果. print(f'write_data函数: {my_list}') # 3. 定义函数, 实现从列表中获取数据. def read_data(): # 休眠 3 秒, 确保 write_data函数执行完毕. time.sleep(3) # 打印结果. print(f'read_data函数: {my_list}') # 在main中测试 if __name__ == '__main__': # 4. 创建进程对象. p1 = multiprocessing.Process(target=write_data) p2 = multiprocessing.Process(target=read_data) # 5. 启动进程 p1.start() p2.start() print('我是main内资源, 看看我执行了几遍!') # 执行一次
执行结果
(默认)主进程等待子进程结束
演示
import multiprocessing, time # 需求: 设置子进程执行3秒, 主进程执行1秒, 观察效果. # 1. 定义方法, 用于关联子进程的. def my_method(): for i in range(10): print(f'工作中... {i}') time.sleep(0.3) # 总休眠时间 = 0.3 * 10 = 3秒 # 2. 在main方法中测试. if __name__ == '__main__': # 3. 创建子进程对象, 并启动. p1 = multiprocessing.Process(target=my_method) p1.start() # 3秒后结束. # 4. 主进程执行1秒后结束. time.sleep(1) # 5. 打印主进程的结束提示. print('主进程 main 执行结束!')
解决
-
设置子进程为守护进程 p1.daemon = True 推荐
类似于: 骑士(守护) 和 公主(非守护)
-
手动关闭子进程 p1.terminate() 不推荐(僵尸进程)
import multiprocessing, time # 需求: 设置子进程执行3秒, 主进程执行1秒, 观察效果. # 1. 定义方法, 用于关联子进程的. def my_method(): for i in range(10): print(f'工作中... {i}') time.sleep(0.3) # 总休眠时间 = 0.3 * 10 = 3秒 # 2. 在main方法中测试. if __name__ == '__main__': # 3. 创建子进程对象, 并启动. p1 = multiprocessing.Process(target=my_method) # 方式1: 设置子进程p1为守护进程, # 非守护进程是: main进程, 所以: 当main进程关闭的时候, 它的守护进程也会关闭. # p1.daemon = True p1.start() # 3秒后结束. # 4. 主进程执行1秒后结束. time.sleep(1) # 方式2: 手动关闭子进程. # 会导致子进程变成僵尸进程, 即: 不会立即释放资源. # 而是交由init进程接管(充当新的父进程), 在合适的时机释放资源. p1.terminate() # 不推荐使用. # 5. 打印主进程的结束提示. print('主进程 main 执行结束!')
二. 线程
介绍
线程是CPU调度资源的最基本单位, 进程是CPU分配资源的基本单位.进程 = 可执行程序, 文件. 即: *.exe = 进程, 微信, QQ都是进程.线程 = 进程的执行路径, 执行单元. 微信这个进程, 可以实现: 和张三聊聊天, 和李四聊天, 查看朋友圈, 微信支付... 车在车道上跑, 有: 单行道, 双车道, 四车道, 八车道...
多线程实现
无论是进程, 还是线程, 都是实现 多任务的一种方式, 目的都是: 充分利用CPU资源, 提高效率.线程的操作步骤: 1. 导包. 2. 创建线程对象. 3. 启动线程.
代码
# 导包 import threading, time # 1.定义函数, 表示: 敲代码. def coding(): for i in range(10): print(f"正在敲代码... {i}") time.sleep(0.1) # 2.定义函数, 表示: 听音乐 def music(): for i in range(10): print(f"正在听音乐... {i}") time.sleep(0.1) # 在main中测试. if __name__ == '__main__': # 3. 创建线程对象, 分别关联上述的两个函数. t1 = threading.Thread(target=coding) t2 = threading.Thread(target=music) # 4. 启动线程. t1.start() t2.start()
带有参数
Thread类(线程类)中的参数 和 Process类(进程类)的参数几乎一致: target: 关联目标函数的. name: 线程名(Thread-1, Thread-2...) 或者 进程名(Process-1, Process-2...). args: 以 元组的 形式传参, 个数及对应的类型都要一致. kwargs: 以 字典的 形式传参, 个数要一致.
# 导包 # from threading import Thread import threading, time # 1. 定义函数, 实现: 小明正在敲第n行代码. def coding(name, num): for i in range(1, num + 1): print(f'{name} 正在敲第 {i} 行代码...') time.sleep(0.1) # 2. 定义函数, 实现: 小明正在听第n首音乐 def music(name, count): for i in range(1, count + 1): print(f'{name} 正在听第 {i} 首音乐......') time.sleep(0.1) # 在main中测试 if __name__ == '__main__': # 3. 创建线程对象. t1 = threading.Thread(name='杨过', target=coding, args=('乔峰', 10)) t2 = threading.Thread(name='大雕', target=music, kwargs={'count': 10, 'name': '慕容复'}) # print(f't1线程的名字: {t1.name}') # print(f't2线程的名字: {t2.name}') # 4. 启动线程. t1.start() t2.start()
线程注意事项
记忆:
多线程的执行具有 随机性(无序性), 其实就是在抢CPU的过程, 谁抢到, 谁执行.
默认情况下: 主线程会等待子线程执行结束再结束.
线程之间 会共享当前进程的 资源.
多线程环境 并发 操作共享资源, 有可能引发安全问题, 需要通过 线程同步(加锁) 的思想来解决.
关于CPU的资源分配, 调度, 思路主要有两种: 1. 均分时间片, 即: 每个进程(线程)占用CPU的时间都是 相等的. 2. 抢占式调度, 谁抢到, 谁执行. Python用的是这种.
无序
# 导包 import threading, time # 需求: 创建多个线程, 多次运行, 观察歌词线程的执行顺序. # 1. 定义函数, 获取线程, 并打印. def get_info(): # 休眠. time.sleep(0.5) # 获取当前的线程对象, 并打印. cur_thread = threading.current_thread() print(f'当前线程是: {cur_thread}') # 在main中测试 if __name__ == '__main__': # 2. 创建多个线程对象. for i in range(10): th = threading.Thread(target=get_info) # 3. 启动线程即可. th.start()
(默认)主线程等待子线程结束
演示
import threading, time # 1. 定义函数, 执行: 3秒. def coding(): for i in range(10): print(f'coding... {i}') time.sleep(0.3) # 总休眠时间 = 0.3 * 10 = 3秒 # 在main中测试 if __name__ == '__main__': # 2. 创建线程对象. th = threading.Thread(target=coding) # 3. 启动线程. th.start() # 4. 设置主线程(main线程), 执行1秒就关闭. time.sleep(1) # 5. 提示即可. print('主线程(main)执行结束了!')
解决
设置子线程为守护线程
import threading, time # 1. 定义函数, 执行: 3秒. def coding(): for i in range(10): print(f'coding... {i}') time.sleep(0.3) # 总休眠时间 = 0.3 * 10 = 3秒 # 在main中测试 if __name__ == '__main__': # 2. 创建线程对象. # 方式1: 设置th线程为: 守护线程. # th = threading.Thread(target=coding, daemon=True) # 推荐使用. # 方式2: setDaemon()函数实现. th = threading.Thread(target=coding) th.setDaemon(True) # 函数已过时, 推荐使用方式1. # 3. 启动线程. th.start() # 4. 设置主线程(main线程), 执行1秒就关闭. time.sleep(1) # 5. 提示即可. print('主线程(main)执行结束了!')
共享全局变量
代码
import threading import time my_list = [] print('main外输出') def write_list(): for i in range(10): my_list.append(i) print(f'{i} 已经添加到列表中') print('write_list:', my_list) def read_list(): time.sleep(1) print('read_list:', my_list) if __name__ == '__main__': th1 = threading.Thread(target=write_list) th2 = threading.Thread(target=read_list) th1.start() th2.start()
执行结果
数据安全
多线程环境 并发 操作共享资源, 有可能引发安全问题, 需要通过 线程同步(加锁) 的思想来解决.参照4
演示:
# 导包 # 导包 import threading import time # 定义全局变量 num = 0 # 定义函数add1()累加 def add1(): global num for i in range(1000000): num += 1 print(f'add1: {num}') # 定义函数add2()累加 def add2(): global num for i in range(1000000): num += 1 print(f'add2: {num}') if __name__ == '__main__': # 创建线程对象 th1 = threading.Thread(target=add1) th2 = threading.Thread(target=add2) # 启动线程 th1.start() # 19328862 th2.start() # 20000000
问题描述
两个线程分别对全局变量累加100W次, 正常应为100W 和 200W的结果,但是结果和想象不一样
原因
多线程环境 并发 操作共享资源, 引发安全问题
-
正常情况:
-
假设 全局变量 global_num = 0
-
此时 线程t1抢到了资源, 执行累加(一次)动作, 累加后: global_num = 1
-
假设 线程t2抢到了资源, 执行累加(一次)动作, 累加后: global_num = 2
-
非正常情况:
-
假设 全局变量 global_num = 0
-
此时 线程t1抢到了资源, 但是还没有来得及执行累加动作时, 被t2线程抢走了资源.
-
此时 线程t2读取到的 global_num = 0
-
此时就会出现 线程t1累加1次, global_num = 1, 线程t2累加1次, global_num = 1
-
综上所述, t1和t2线程一共累加了2次, 但是 global_num的值 只加了1
-
之所以会有这样的情况, 原因是: 1个线程在执行某1个完整动作期间, 可以被别的前程抢走资源, 就有可能引发安全问题.
解决方案
采用 线程同步 的思想, 即: 加锁
# 导包 import threading import time # 定义全局变量 num = 0 mutex = threading.Lock() # 定义函数add1()累加 def add1(): # 加锁 mutex.acquire() global num for i in range(1000000): num += 1 print(f'add1: {num}') # 释放锁 mutex.release() # 定义函数add2()累加 def add2(): # 加锁 mutex.acquire() global num for i in range(1000000): num += 1 print(f'add2: {num}') # 释放锁 mutex.release() if __name__ == '__main__': # 创建线程对象 th1 = threading.Thread(target=add1) th2 = threading.Thread(target=add2) # 启动线程 th1.start() # 19328862 th2.start() # 20000000
线程同步
用于解决多线程 并发 操作共享变量的安全问题的, 保证同一时刻只有1个线程操作共享变量.
锁的概述
互斥锁:
对共享数据进行锁定,保证同一时刻只有一个线程去操作。
注:
互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程进行等待,等锁使用完释放后,其它等待的线程再去抢这个锁。
使用步骤
-
创建锁
-
在合适的地方加锁
-
在合适的地方释放锁
代码
# 导包 import threading, time # 1. 定义全局变量. global_num = 0 # 创建锁. mutex = threading.Lock() # 互斥锁. # mutex2 = threading.Lock() # 互斥锁. # 2. 定义函数1, 对全局变量 global_num累加100W次. def get_sum1(): # 加锁. mutex.acquire() # 声明变量为 全局变量. global global_num # 具体的累加动作. for i in range(1000000): global_num += 1 # 解锁 mutex.release() # 累加完毕后, 打印结果. print(f'get_sum1函数执行完毕, global_num = {global_num}') # 3. 定义函数2, 对全局变量 global_num累加100W次. def get_sum2(): # 加锁. # mutex2.acquire() mutex.acquire() # 声明变量为 全局变量. global global_num # 具体的累加动作. for i in range(1000000): global_num += 1 # 解锁 # mutex2.release() mutex.release() # 累加完毕后, 打印结果. print(f'get_sum2函数执行完毕, global_num = {global_num}') # main函数, 测试 if __name__ == '__main__': # 4. 创建线程对象. t1 = threading.Thread(target=get_sum1) t2 = threading.Thread(target=get_sum2) # 5. 启动线程. t1.start() t2.start()
注意事项
必须使用同一把锁, 否则可能锁不住(同一块cpu有两个锁两个门)
在合适的地方释放锁, 否则可能死锁
进程与线程的区别
关系
线程依附于进程, 没有进程就没有线程
一个进程默认提供一个线程, 可以创建多个进程
区别
-
进程间数据隔离
-
线程数据共享,但是要注意资源竞争 ,可以加互斥锁
-
进程比线程的资源开销大
-
线程是CPU调度资源的最基本单位, 进程是CPU分配资源的基本单位
-
线程不能独立执行, 必须存在于进程中
-
python中 多进程 比 单进程多线程 稳定
优缺点
-
进程:
优点: 可以使用多核
缺点: 资源开销大
-
线程:
优点: 资源开销小
缺点: 不可以使用多核
总结
线程依赖于进程
进程数据隔离, 线程数据共享
进程资源开销比线程资源开销大, 所以相对更稳定
无论多线程还是多进程都可以实现多任务, 目的都是: 充分利用cpu资源, 提高程序的执行效率