前言
- 之前所写的程序都是单任务的,也就是说一个函数或者方法执行完成 , 另外一个函数或者方法才能执行 . 要想实现多个任务同时执行就需要使用多任务。
一、多任务
1.1 多任务概述
"""
1、概述:
多任务指的是 多个任务同时执行.
2、目的:
节约资源, 充分利用CPU资源, 提高效率.
"""
1.2 多任务的两种表现形式
1.2.1 并发
- 针对于单核CPU来讲的, 如果有多个任务同时请求执行, 但是同一瞬间CPU只能执行1个(任务), 于是就安排它们交替执行.
- 因为时间间隔非常短(CPU执行速度太快了), 我们看起来好像是同时执行的, 其实不是.
1.2.2 并行
- 针对于多核CPU来讲的. 多个任务可以同时执行.
1.3 多任务的优点
- 多任务执行能够充分利用cpu资源,提高程序执行效率
二、进程
2.1 进程介绍
- 进程(Process)是CPU资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位。
- 通俗理解:一个正在运行的程序就是一个进程 。 例如:正在运行的qq ,微信等他们都是一个进程
2.2 多进程完成多任务
-
多进程的实现步骤:
- 1、导包。
- 2、创建进程对象, 关联: 要执行的函数。
- 3、启动进程。
2.2.1 多进程代码演示
代码如下:
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()
2.2.2 带参数的多进程代码演示
代码如下:
"""
1、进程的参数解释, 即: multiprocessing模块的 Process类的 的参数:
target: 用于关联 进程要执行的任务的.
name: 进程名, 默认是: Process-1, Process-2,......Process-n, 可以手动修改, 一般不改.
args: 可以通过 元组 的形式传递参数, 实参的个数 及 对应的数据类型 要和 形参的个数及类型 一致.
kwargs: 可以通过 字典 的形式传递参数, 实参的个数 要和 形参的个数 一致.
2、进程传参的两种方式是什么?
a.元组方式传参 :元组方式传参一定要和任务函数的参数顺序保持一致。
b.字典方式传参:字典方式传参字典中的key一定要和任务函数的参数保持一致
"""
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__':
# 6. 看看我在不同的地方, 有什么结果.
for i in range(200):
time.sleep(0.01)
print(f'看看我在什么地方---{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'看看我在什么地方---{i}')
2.3 获取进程编号
"""
1、概述:
在操作系统中, 每个进程都有自己唯一的ID, 且当前进程被终止时, 该ID会被回收, 即: ID是可以重复使用的.
2、目的:
1. 查找子进程是由那个父进程创建的, 即: 找子进程和父进程的ID.
2. 方便我们维护进程, 例如: kill -9 pid值 可以强制杀死进程.
3、获取进程id的方式:
获取当前进程的ID:
方式1: os模块的 getpid()方法
方式2: multiprocessing模块的 pid属性
获取当前进程的父进程的pid:
os模块的 getppid()方法 parent process: 父进程
"""
代码如下(示例):
# 案例: 实现一边敲着第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()}')
2.4 进程需要注意的点
2.4.1 进程间的数据是相互隔离的
解释
"""
大白话解释:
1. 微信 和 QQ都是进程, 它们之间的数据都是 相互隔离的. 即: 微信不能直接访问QQ的数据, QQ也不能直接访问微信的数据.
2. 下述代码你会发现, 多个子进程相当于把 父进程的资源全部拷贝了一份(注意: main外资源), 即: 子进程相当于父进程的副本.
需求: 定义全局变量 my_list = [], 搞两个进程, 分别实现往里边添加数据, 从里边读取数据, 并观察结果.
"""
代码如下(示例):
# 案例: 演示进程之间 数据是相互隔离的.
# 导包
import multiprocessing, time
# 1. 定义全局变量.
my_list = []
# 2. 定义函数 write_data(), 往: 列表中 添加数据.
def write_data():
# 为了让效果更明显, 我们添加多个值.
for i in range(1, 6):
my_list.append(i) # 具体添加元素到列表的动作.
print(f'add: {i}') # 打印添加细节(过程)
print(f'write_data函数: {my_list}') # 添加完毕后, 打印列表结果
# 3. 定义函数 read_data(), 从: 列表中 读取数据.
def read_data():
# 休眠一会儿, 确保 write_data()函数执行完毕.
time.sleep(3)
print(f'read_data函数: {my_list}') # 打印列表结果
# 4. 在main中测试
if __name__ == '__main__':
# 4.1 创建两个进程对象.
p1 = multiprocessing.Process(target=write_data) # 进程1, 例如: QQ
p2 = multiprocessing.Process(target=read_data) # 进程2, 例如: 微信
# 4.2 开启进程.
p1.start()
p2.start()
# print('看看我打印了几遍? ') # main函数内的内容, 只执行一次.
print('看看我打印了几遍? ') # main外资源, 自身要执行一次, p1进程拷贝1遍, p2进程拷贝1遍 = 3次
2.4.2 主进程会等待子进程执行结束(默认的)
解释
"""
案例:
演示 默认情况下, 主进程会等待子进程执行结束在结束.
需求:
创建1个子进程, 子进程执行完需要3秒钟, 让主进程1秒就结束, 观察: 整个程序是立即结束, 还是会等待子进程结束再结束.
结论:
1. 默认情况下, 主进程会等待所有子进程执行结束再结束.
2. 问: 如何实现, 当主进程结束的时候, 子进程也立即结束呢?
方式1: 设置子进程为守护进程. 类似于: 公主 和 守护骑士.
方式2: 手动关闭子进程.
"""
代码演示主进程关闭 ,但是在会等待子进程执行完毕
# 导包
import multiprocessing, time
# 1. 定义函数work(), 要被: 子进程关联.
def work():
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=work)
# 4. 开启子进程.
p1.start()
# 5. 让主进程(main进程)休眠1秒.
time.sleep(1)
# 6. 主进程结束, 打印提示语句.
print('主进程执行结束了!')
2.4.2 修改 主进程关闭 子进程也跟着立刻关闭
解释
"""
案例:
演示 默认情况下, 主进程会等待子进程执行结束在结束.
需求:
创建1个子进程, 子进程执行完需要3秒钟, 让主进程1秒就结束, 观察: 整个程序是立即结束, 还是会等待子进程结束再结束.
结论:
1. 默认情况下, 主进程会等待所有子进程执行结束再结束.
2. 问: 如何实现, 当主进程结束的时候, 子进程也立即结束呢?
方式1: 设置子进程为守护进程. 类似于: 公主 和 守护骑士.
方式2: 手动关闭子进程.
细节:
1. 当非守护进程结束的时候, 它的所有守护进程都会立即终止, 且释放资源.
2. 如果是 terminate()方式, 子进程主动结束自己, 则会变成僵尸进程, 不会立即释放资源.
"""
代码演示 主进程结束 子进程也立刻结束
# 导包
import multiprocessing, time
# 1. 定义函数work(), 要被: 子进程关联.
def work():
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=work)
# 核心细节: 设置p1为守护线程, 因为p1的父进程是main, 所以p1是main的守护进程.
# p1.daemon = True
# 4. 开启子进程.
p1.start()
# 5. 让主进程(main进程)休眠1秒.
time.sleep(1)
# 方式2: 在主进程执行结束前, 子进程主动自己关闭自己.
p1.terminate() # 不推荐用, 会变成僵尸进程, 不会主动释放资源, 过段时间会交由(init进程, 充当父进程),来释放资源.
# 6. 主进程结束, 打印提示语句.
print('主进程执行结束了!')
三、线程
3.1 线程介绍
"""
1、线程:
CPU调度资源的最小单位, 即: 进程的执行单元, 执行路径.
可以理解为: 车道
2、多进程:
多个*.exe, 例如: 微信.exe, QQ.exe, 即: 不同的软件.
3、多线程:
同一个进程, 有多条执行路径, 例如: 微信.exe -> 查看钱包, 和张三聊天, 和李四聊天, 查看照片...
"""
代码演示 用多线程思路, 实现: 一边写代码, 一边听音乐.
# 导包
import threading, time
# 1. 定义函数coding(), 表示: 敲代码
def coding():
for i in range(10):
print(f'正在敲第 {i} 遍代码! -------')
time.sleep(0.1)
# 2. 定义函数music(), 表示: 听音乐.
def music():
for i in range(10):
print(f'正在听第 {i} 首音乐!')
time.sleep(0.1)
# 3. 在main中测试.
if __name__ == '__main__':
# 4. 创建两个线程对象.
t1 = threading.Thread(target=coding)
t2 = threading.Thread(target=music)
# 5. 开启线程.
t1.start()
t2.start()
3.2 多线程带参数
代码演示 用多线程思路, 实现: 一边写代码, 一边听音乐.(带参数版)
# 导包
import threading, time
# 1. 定义函数coding(), 表示: 敲代码
def coding(name, num):
for i in range(num):
print(f'{name} 正在敲第 {i} 遍代码! -------')
time.sleep(0.1)
# 2. 定义函数music(), 表示: 听音乐.
def music(name, count):
for i in range(count):
print(f'{name} 正在听第 {i} 首音乐!', end='\n')
time.sleep(0.1)
# 3. 在main中测试.
if __name__ == '__main__':
# 4. 创建两个线程对象.
# Thread()类中的参数如下:
# target 关联要执行的目标函数对象.
# name 设置线程名
# args 以元组形式传参, 个数, 顺序都要保持一致.
# kwargs 以字典形式传参, 个数, 参数名(键名和形参名)保持一致.
t1 = threading.Thread(target=coding , args = ('张三', 9))
t2 = threading.Thread(target=music, kwargs={'name': '李四', 'count': 10})
# 5. 开启线程.
t1.start()
t2.start()
3.3 多线程注意的点
3.3.1 多线程执行顺序
解释
"""
结论:
1. 多线程的执行具有 随机性, 因为: 线程是由CPU调度的.
2. 调度资源常用的两种方式:
抢占式调度: 谁抢到资源, 谁执行.
Python, Java用的都是这种思路.
均分时间片: 每个任务获取CPU的时间都是固定的.
"""
代码演示多线程代码 的执行顺序.
# 导包
import threading
import time
# 1. 定义函数get_info(), 用来打印: 线程信息, 看看当前是哪个线程在执行.
def get_info():
# 休眠一会儿, 让效果更明显.
time.sleep(0.1)
# 1.1 获取当前的线程对象.
ct = threading.current_thread()
# 1.2 打印当前线程对象即可.
print(f'当前的线程对象是: {ct}')
# 2. 在main中测试.
if __name__ == '__main__':
# 3. 为了让效果更明显, 创建多个线程.
# t1 = threading.Thread(target=get_info)
# t2 = threading.Thread(target=get_info)
#
# t1.start()
# t2.start()
for i in range(10):
t1 = threading.Thread(target=get_info)
t1.start()
3.3.2 多线程-等待子线程结束再结束
解释
"""
结论:
1. 默认情况下, 主线程会等待所有子线程执行结束再结束.
2. 问: 如何实现, 当主线程结束的时候, 子线程也立即结束呢?
设置子线程为守护线程. 类似于: 公主 和 守护骑士.
"""
代码 演示 默认情况下, 主线程会等待子线程执行结束在结束.
"""
需求:
创建1个子线程, 子线程执行完需要3秒钟, 让主线程1秒就结束, 观察: 整个程序是立即结束, 还是会等待子线程结束再结束.
"""
# 导包
import threading, time
# 1. 定义函数work(), 要被: 子线程关联.
def work():
for i in range(10):
print(f'工作中... {i}')
time.sleep(0.3) # 总计休眠时间: 0.3 * 10 = 3秒
# 2. 在main函数中测试.
if __name__ == '__main__':
# 3. 创建子线程.
t1 = threading.Thread(target=work)
# 4. 开启子线程.
t1.start()
# 5. 让主线程(main线程)休眠1秒.
time.sleep(1)
# 6. 主线程结束, 打印提示语句.
print('主线程执行结束了!')
3.3.3 多线程-守护线程
代码演示 当非守护线程结束的时候, 和它关联的守护线程, 也会立即结束, 释放资源.
# 导包
import threading, time
# 1. 定义函数work(), 要被: 子线程关联.
def work():
for i in range(10):
print(f'工作中... {i}')
time.sleep(0.3) # 总计休眠时间: 0.3 * 10 = 3秒
# 2. 在main函数中测试.
if __name__ == '__main__':
# 3. 创建子线程.
# 守护线程, 格式1: 创建线程对象的时候, 直接指定.
# t1 = threading.Thread(target=work, daemon=True)
t1 = threading.Thread(target=work)
# 守护线程, 格式2: 创建线程对象后, 通过 daemon属性设置.
# t1.daemon = True
# 守护线程, 格式3: 创建线程对象后, 通过 setDaemon()函数设置.
t1.setDaemon(True) # 类似于以前我们学的: get_name(), set_name()
# 4. 开启子线程.
t1.start()
# 5. 让主线程(main线程)休眠1秒.
time.sleep(1)
# 6. 主线程结束, 打印提示语句.
print('主线程执行结束了!')
3.3.4 多线程共享全局变量
解释
"""
结论:
1. 进程之间, 数据相互隔离. 进程 = 软件.
2. 线程之间, 数据共享.
"""
代码演示 同一进程的多个线程, 可以共享 该进程的资源.
# 导包
import threading, time
# 1. 定义全局变量.
my_list = []
# 2. 定义函数write_data(), 实现: 往列表中添加数据.
def write_data():
# 为了效果更明显, 我们添加多个元素.
for i in range(1, 6):
# time.sleep(0.1)
# 往列表中添加元素.
my_list.append(i)
# 打印添加的细节
print(f'add: {i}')
# 添加之后, 打印结果.
print(f'write_data函数: {my_list}')
# 3. 定义函数read_data(), 实现: 从列表中读取数据.
def read_data():
time.sleep(2) # 为了效果更好看, 加入休眠线程.
print(f'read_data函数: {my_list}')
# 4. main函数中进行测试.
if __name__ == '__main__':
# 4.1 创建两个线程对象, 分别关联: write_data(), read_data()函数.
t1 = threading.Thread(target=write_data)
t2 = threading.Thread(target=read_data)
# 4.2 启动线程.
t1.start()
t2.start()
3.3.5 数据安全
解释
"""
案例: 线程可以共享全局变量, 同时操作, 可能会出现: 安全问题.
1、遇到的问题:
两个线程同时操作共享变量, 进行累加操作, 出现: 累计次数"不够"的情况, 我们要的效果是: get_sum1: 100W, get_sum2:200W, 但是结果不是.
2、产生原因:
正常情况:
假设g_num = 0
第1次循环: t1线程抢到资源, 对其累加1, 操作后 g_num = 1, t1线程本次操作完毕.
第2次循环: 假设t2线程抢到了资源, 此时 g_num = 1, 操作之后(累加1), g_num = 2, t2线程本次操作完毕.
这个是正常情况.
非正常情况:
1. 假设 g_num = 0
2. 假设 t1 线程抢到了资源, 在还没有来得及对 g_num(全局变量)做 累加操作的时候, 被t2线程抢走了资源.
3. t2线程也会去读取 g_num(全局变量)的值, 此时 g_num = 0
4. 之后t1 和 t2线程分别开始了 累加操作:
t1线程, 累加之后, g_num = 0 -> g_num = 1
t2线程, 累加之后, g_num = 0 -> g_num = 1
5. 上述一共累加了 2次, 但是 g_num的值最终只 累加了 1.
"""
代码如下(示例):
import threading
# 需求: 定义两个函数, 分别对全局变量累加100W次, 并打印观察程序的最终运行结果.
# 1. 定义全局变量.
g_num = 0
# 2. 定义函数 get_sum1(), 实现对全局变量累加 100W次
def get_sum1():
for i in range(1000000):
global g_num
g_num += 1 # 全局变量 + 1
# 程序计算完毕后, 打印结果.
print(f'get_sum1函数计算结果: {g_num}') # 100W
# 3. 定义函数 get_sum2(), 实现对全局变量累加 100W次
def get_sum2():
for i in range(1000000):
global g_num
g_num += 1 # 全局变量 + 1
# 程序计算完毕后, 打印结果.
print(f'get_sum2函数计算结果: {g_num}') # 200W
# 4. 测试
if __name__ == '__main__':
# 4.1 创建两个线程对象, 分别关联: 两个函数.
t1 = threading.Thread(target=get_sum1)
t2 = threading.Thread(target=get_sum2)
# 4.2 启动线程.
t1.start()
t2.start()
3.4 互斥锁的使用
解释
"""
为了解决上述 数据安全出现的问题,我们采用的解决方案:
采用 线程同步 解决.
线程同步解释:
1、概述:
线程同步, 也叫线程安全, 它是用来解决 多线程操作共享变量, 可能出现非法值的问题的.
2、解决思路:
加锁.
3、步骤:
1). 创建锁.
2). 加锁.
3). 释放锁.
细节:
1. 必须使用同一把锁, 否则可能出现锁不住的情况.
2. 选择合适的时机要释放锁, 否则可能出现死锁的情况.
"""
代码如下(示例):
import threading
# 需求: 定义两个函数, 分别对全局变量累加100W次, 并打印观察程序的最终运行结果.
# 1. 定义全局变量.
g_num = 0
# 核心细节, 创建: 锁(互斥锁)
mutex = threading.Lock() # mutex是变量名, 符合规范即可. mutex单词 是 互斥的意思.
# mutex2 = threading.Lock() # mutex是变量名, 符合规范即可. mutex单词 是 互斥的意思.
# 2. 定义函数 get_sum1(), 实现对全局变量累加 100W次
def get_sum1():
# 加锁
mutex.acquire()
for i in range(1000000):
global g_num
g_num += 1 # 全局变量 + 1
# 程序计算完毕后, 打印结果.
# 解锁
mutex.release()
print(f'get_sum1函数计算结果: {g_num}') # 100W
# 3. 定义函数 get_sum2(), 实现对全局变量累加 100W次
def get_sum2():
# 加锁
mutex.acquire()
for i in range(1000000):
global g_num
g_num += 1 # 全局变量 + 1
# 程序计算完毕后, 打印结果.
# 解锁
mutex.release()
print(f'get_sum2函数计算结果: {g_num}') # 200W
# 4. 测试
if __name__ == '__main__':
# 4.1 创建两个线程对象, 分别关联: 两个函数.
t1 = threading.Thread(target=get_sum1)
t2 = threading.Thread(target=get_sum2)
# 4.2 启动线程.
t1.start()
t2.start()
四、进程和线程的对比
4.1 关系对比
- 线程是依附在进程里面的,没有进程就没有线程
- 一个进程默认提供一条线程,进程可以创建多个线程
4.2 区别对比
- 进程之间不共享全局变量
- 线程之间共享全局变量,但是要注意资源竞争的问题,解决办法: 互斥锁
- 创建进程的资源开销要比创建线程的资源开销要大
- 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位
- 线程不能够独立执行,必须依存在进程中
- Python中多进程开发比单进程多线程开发稳定性要强
4.3 优缺点对比
-
进程优缺点:
- ——优点:可以用多核
- ——缺点:资源开销大 线程优缺点:
- ——优点:资源开销小
- ——缺点:不能使用多核
总结
- 我们将多进程跟多线程放在一起对比学习,能够更清楚的发现进程跟线程的相似点与不同点。