多进程,多线程
1、多任务
概述
多个任务同时执行
目的
节约资源,充分利用CPU资源,提高效率
表现形式
并发:
针对于单核CPU来讲的, 如果有多个任务同时请求执行, 但是同一瞬间CPU只能执行1个(任务), 于是就安排它们交替执行.
因为时间间隔非常短(CPU执行速度太快了), 我们看起来好像是同时执行的, 其实不是.
并行:
针对于多核CPU来讲的. 多个任务可以同时执行.
2、多进程
(一)、概述
多个任务同时进行
(二)、多进程代码
import multiprocessing, time
# 需求: 模拟一遍写代码, 一遍听音乐.
# 1. 定义函数, 表示: 写代码.
def coding():
for i in range(1, 21):
# 为了让效果更明显, 加入: 休眠线程.
time.sleep(0.01)
print(f"正在写代码... {i}", end='\n')
# 2. 定义函数, 表示: 听音乐.
def music():
for i in range(1, 21):
# 为了让效果更明显, 加入: 休眠线程.
time.sleep(0.01)
print(f"正在听音乐------ {i}")
# 3. 在main函数中测试.
if __name__ == '__main__':
# 4. 创建两个进程对象, 分别关联上述的两个函数.
p1 = multiprocessing.Process(target=coding)
p2 = multiprocessing.Process(target=music)
# 5. 启动进程.
p1.start()
p2.start()
(三)、带参数的多进程
参数解释
进程的参数解释, 即: multiprocessing模块的 Process类的 的参数:
target: 用于关联 进程要执行的任务的.
name: 进程名, 默认是: Process-1, Process-2,......Process-n, 可以手动修改, 一般不改.
args: 可以通过 元组 的形式传递参数, 实参的个数 及 对应的数据类型 要和 形参的个数及类型 一致.
kwargs: 可以通过 字典 的形式传递参数, 实参的个数 要和 形参的个数 一致.
代码
import multiprocessing, time
# 需求: 使用多进程来模拟小明一边写num行代码, 一边听count首音乐.
# 1. 小明写num行代码
def code(name, num):
for i in range(1, num):
# 为了让效果更明显, 加入: 休眠线程.
time.sleep(0.01)
print(f"{name} 正在写第 {i} 行代码... ")
# 2. 小明听count首音乐
def music(name, count):
for i in range(1, count):
# 为了让效果更明显, 加入: 休眠线程.
time.sleep(0.01)
print(f"{name} 正在听第 {i} 首音乐------ ")
# 3. 在main函数中编写测试代码.
if __name__ == '__main__':
# 7. 看看我在不同的地方, 有什么结果.
for i in range(200):
time.sleep(0.01)
print(f'去码头整点薯条 --- 666! {i}')
# 4. 创建进程对象.
p1 = multiprocessing.Process(target=code, args=("小明", 20), name='QQ进程')
# print(f'p1进程的名字: {p1.name}')
p2 = multiprocessing.Process(target=music, kwargs={'count': 20, 'name': '小明'}, name='微信进程')
# print(f'p2进程的名字: {p2.name}')
# 5. 启动进程.
p1.start()
p2.start()
# 6. 看看我在不同的地方, 有什么结果.
# for i in range(200):
# time.sleep(0.01)
# print(f'去码头整点薯条 --- 666! {i}')
注意:6处这里的代码会与p1,p2抢占资源,而7处的代码则不会。
因为在启动进程的同时,会在内存空间中自定义出一个额外的栈,也就是说p1.start()、p2.start()这两步,分别创建了两个自定义栈01、02,此时三个栈(01,02,main)同时运行,抢占资源。而7处的代码因为在创建自定义栈之前,所以不会出现抢占情况。
(四)、获取进程的id
概述
在操作系统中, 每个进程都有自己唯一的ID, 且当前进程被终止时, 该ID会被回收, 即: ID是可以重复使用的
目的
-
查找子进程是由那个父进程创建的, 即: 找子进程和父进程的ID.
-
方便我们维护进程, 例如: kill -9 pid值 可以强制杀死进程.
获取进程id方式
获取当前进程的ID:
方式1: os模块的 getpid()方法
方式2: multiprocessing模块的 pid属性
获取当前进程的父进程的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()}')
(五)、进程间数据相互隔离
概述
-
进程之间数据是相互隔离的. 例如: 微信(进程) 和 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内资源, 看看我执行了几遍!')
(六)、主进程与子进程
(1)、默认情况下主进程会等待子进程结束再结束
问: 如果要实现主进程结束时, 子进程也同步结束, 怎么办?
答:
1. 设置子进程为 守护进程. 类似于: 骑士(守护) 和 公主(非守护)
2. 手动关闭子进程.
代码
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 执行结束!')
(2)、子进程随着主进程关闭而关闭
格式
-
进程名称.daemon = True # 设置子进程p1为守护进程, 非守护进程是: main进程, 所以: 当main进程关闭的时候, 它的守护进程也会关闭.
-
进程名称.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 执行结束!')
3、多线程
(一)、概述
线程是CPU调度资源的最基本单位, 进程是CPU分配资源的基本单位.
进程 = 可执行程序, 文件.
即: *.exe = 进程, 微信, QQ都是进程.
线程 = 进程的执行路径, 执行单元.
微信这个进程, 可以实现: 和张三聊聊天, 和李四聊天, 查看朋友圈, 微信支付...
(二)、区别
-
关系区别:
线程是依赖进程的, 且1个进程至少会有1个线程. -
特点区别:
- 进程间数据是相互隔离的, 线程间数据是可以共享的.
- 线程间同时操作共享数据, 可能引发安全问题, 需要用到 互斥锁 的思路解决
- 进程的资源开销要比 线程的资源开销大.
- 多进程程序 比 单进程多线程程序要更加的稳定.
-
优缺点:
进程: 可以实现多核操作, 资源开销较大.
线程: 不能使用多核, 资源开销相对较小.
大白话:
1. 线程是依赖进程的.
2. 进程数据隔离, 线程数据共享.
3. 进程资源开销比线程资源开销大, 所以相对更稳定.
4. 无论是多线程 还是多进程, 都可以实现 多任务, 目的都是: 充分利用CPU资源, 提高程序的执行效率.
(三)、多线程代码
# 导包
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()
(五)、线程执行具有随机性
细节
1. 多线程的执行具有 随机性(无序性), 其实就是在抢CPU的过程, 谁抢到, 谁执行.
2. 默认情况下: 主线程会等待子线程执行结束再结束.
3. 线程之间 会共享当前进程的 资源.
4. 多线程环境 并发 操作共享资源, 有可能引发安全问题, 需要通过 线程同步(加锁) 的思想来解决.
资源分配
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()
(六)、主线程与子线程
QA
问: 如何实现子线程随着主线程的结束而结束?
答:设置 子线程为 守护线程.
例如
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, time
# 1. 定义全局变量
my_list = []
# 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():
# 为了效果更明显, 我们加入休眠线程, 确保 添加动作 执行完毕了.
time.sleep(2)
print(f'read_data函数: {my_list}')
# 在main中测试.
if __name__ == '__main__':
# 4. 创建线程对象.
t1 = threading.Thread(target=write_data)
t2 = threading.Thread(target=read_data)
# 5. 启动线程.
t1.start()
t2.start()
(八)、多线程引发的安全问题以及解决
(1)、安全问题
描述
问题描述: 两个线程分别对全局变量累加100W次, 我们想看到100W 和 200W的结果, 但是结果和我们想的不一样.
产生原因: 多线程 并发 操作共享(全局)变量, 引发 安全问题, 具体如下:
正常情况:
1. 假设 全局变量 global_num = 0
2. 此时 线程t1抢到了资源, 执行累加(一次)动作, 累加后: global_num = 1
3. 假设 线程t2抢到了资源, 执行累加(一次)动作, 累加后: global_num = 2
4. 这个是正常的情况.
非正常情况:
1. 假设 全局变量 global_num = 0
2. 此时 线程t1抢到了资源, 但是还没有来得及执行累加动作时, 被t2线程抢走了资源.
3. 此时 线程t2读取到的 global_num = 0
4. 此时就会出现 线程t1累加1次, global_num = 1, 线程t2累加1次, global_num = 1
5. 综上所述, t1和t2线程一共累加了2次, 但是 global_num的值 只加了1
6. 之所以会有这样的情况, 原因是: 1个线程在执行某1个完整动作期间, 可以被别的前程抢走资源, 就有可能引发安全问题.
例如
# 导包
import threading, time
# 1. 定义全局变量.
global_num = 0
# 2. 定义函数1, 对全局变量 global_num累加100W次.
def get_sum1():
# 声明变量为 全局变量.
global global_num
# 具体的累加动作.
for i in range(1000000):
global_num += 1
# 累加完毕后, 打印结果.
print(f'get_sum1函数执行完毕, global_num = {global_num}')
# 3. 定义函数2, 对全局变量 global_num累加100W次.
def get_sum2():
# 声明变量为 全局变量.
global global_num
# 具体的累加动作.
for i in range(1000000):
global_num += 1
# 累加完毕后, 打印结果.
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()
(2)、问题解决
解决方案
解决方案:
采用 线程同步 的思想解决, 即: 加锁.
线程同步:
概述:
用于解决多线程 并发 操作共享变量的安全问题的, 保证同一时刻只有1个线程操作共享变量.
使用步骤:
1. 创建锁.
2. 在合适的地方: 加锁.
3. 在合适的地方: 释放锁.
细节:
1. 必须使用同一把锁, 否则可能出现锁不住的情况.
2. 必须在合适的时机释放锁, 否则可能出现死锁的情况.
例如
# 导包
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()