此博客仅用于分享本人在求职路上的学习内容以及过程(第五天)。
多任务介绍
线程与进程都是执行多任务的方式,只是其执行方式有区别,因此在了解线程与进程之前,需要先了解什么是多任务。
概念
:多任务(Multitasking)是指计算机系统或人工智能模型同时处理多个任务的能力。这一概念广泛应用于操作系统、机器学习、人机交互等领域,其核心目标是提高效率、资源利用率和系统性能。
作用
:多任务能够提高系统的处理效率,提高系统的资源利用率,提高系统的性能。
多任务即能够在一段时间内完成多个任务,然而这种完成多个任务的形式却可以分为如下两种:
- 并发:在同一时间交替执行多个任务。
- 并行:在一段时间内,同时执行多个任务。
进程
概念
:进程是CPU分配资源的最小单位,是操作系作资源分配和调度系统的基本单位。进程是操作系统中一个正在运行的程序,进程之间是相互独立的,每个进程都有自己的内存空间,进程之间可以通过通信机制进行信息传递。
作用
:
- 资源隔离与保护:
- 独立的空间:每个进程都有自己的虚拟地址,进程之间互不干扰。
- 系统资源隔离:文件、网络端口等资源都是由进程单独管理,避免冲突。
- 实现多任务并发:
- 伪并行(单核CPU):通过时间片轮转,操作系统快速切换进程,用户感知为“同时运行多个程序”(如边下载文件边编辑文档)。
- 真并行(多核CPU):不同进程可分配到不同CPU核心上真正同时执行(如游戏渲染和后台杀毒扫描同步进行)。
- 提高系统效率和资源利用率:
- 阻塞时的CPU利用:当某个进程等待I/O(如读取磁盘)时,CPU可立即切换至其他进程,避免空闲。
- 优先级调度:操作系统可根据进程优先级分配资源(如系统进程优先于用户进程)。
- 提供程序执行的标准化环境:
- 抽象硬件细节:进程通过操作系统接口访问硬件(如文件系统、网络),无需直接处理底层差异。
- 错误隔离:一个进程崩溃通常不会影响其他进程(如浏览器标签页崩溃不会关闭整个系统)。
- 支持进程间通信(IPC)与协作:
- 通信机制:管道、消息队列、共享内存等允许进程交换数据(如视频播放器从下载进程获取数据)。
- 同步机制:信号量、锁等协调多个进程对共享资源的访问(如避免多个进程同时打印文档)。
- 用户与系统的交互接口
- 任务管理:用户可通过进程ID(PID)查看或控制程序(如
kill
命令终止进程)。 - 权限控制:操作系统基于进程所属用户权限限制其操作(如普通用户进程无法修改系统文件)。
- 任务管理:用户可通过进程ID(PID)查看或控制程序(如
多进程
多进程的工作方式
多进程的工作方式,本文将通过一张图进行说明:
如图所示程序,程序运行会默认创建一个进程,这个默认创建的进程我们称之为主进程(左侧图),在运行后又创建了一个进程这个新创建的进程我们称之为子进程。
多进程的创建与传参
多进程的创建步骤可以分为三步:
- 导入模块。
- 通过过程类,实例化进程对象。
- 启动进程执行任务。
通过代码演示如下:
import time
import multiprocessing
# 1、定义函数:表示代码
def coding():
for i in range(10):
print(f"正在写代码...{i}")
# 设置睡眠时间使多进程效果明显
time.sleep(1)
# 2、定义函数:表示听音乐
def music():
for i in range(10):
print(f"正在听音乐...{i}")
time.sleep(1)
# 一心可二用
if __name__ == '__main__':
# 3、创建进程对象
m1 = multiprocessing.Process(target=coding, args=())
m2 = multiprocessing.Process(target=music, )
# 4、启动进程
m1.start()
m2.start()
多进程获取进程ID
在Python程序中,为了区分多程序的进程,每一个进程都有唯一与之对应的进程ID,在代码中,我们可以通过os
模块内的os.getpid()
、multiprocessing
模块内multiprocessing.current_process().pid
以及进程名.pid
的方法查看当前进程ID,还可以通过os.getppid()
和multiprocessing.current_process().pid
的方法查看当前进程的父进程ID。
注意:os.getpid()和multiprocessing.current_process().pid均得写在对应函数内部方可获取对应进程ID,若multiprocessing.current_process().pid在写在主函数中,则是获取的主函数ID。若os.getppid()写在主函数中,则是获取的cmd进程的ID号,写在函数内,则是获取的主进程ID。
多进程注意点
在多进程编程中,我们需要注意以下两点:
- 进程之间是互相隔离的,全局变量不共享。
- 父进程会等待所有子进程结束再结束。
全局变量不共享
在Python代码中,创建一个进程对象的理论如下:Python在创建进程对象后,会复制一遍需要执行的代码,然后独立于其它进程运行,因此子进程与子进程之间以及子进程与主进程之间的全局变量不共享。
下面将通过一段代码实例来演示:
# 需求:在不同进程中,修改列表my_list=[],并新增元素,观察结果
import multiprocessing
import time
# 1.定义全局变量,表示共享资源
my_list = [1, 2, 3]
print("观察我执行了几次")
# 2、定义函数:write_data(),往列表中添加数据
def write_data():
my_list.clear()
for i in range(3):
# 添加数据
my_list.append(i)
print(f"写入数据:{i}")
# time.sleep(0.5)
# 添加完毕后,打印列表中的所有元素
print(f"write_data()函数结果:{my_list}")
# 3、定义读函数:read_data()
def read_data():
time.sleep(3)
print(f"read_data()函数结果:{my_list}")
if __name__ == '__main__':
# 4、创建两个对象分别关联两个函数
p1 = multiprocessing.Process(target=write_data)
p2 = multiprocessing.Process(target=read_data)
p1.start()
time.sleep(3)
p2.start()
print(my_list)
通过代码的运行结果不难看出,print("观察我执行了几次")
这段代码一共执行了3次,且write_data()函数中的my_list和read_data()函数中的my_list两个列表读到的值也不同。因此可以判断出所有进程(主进程和所有子进程)之间是相互独立的。
主进程与子进程的结束关系
在有多进程的多任务中,我们一般默认主程序会等待子程序全部结束后再结束。通过上面的代码实例中的print("观察我执行了几次")
的输出位置也能看出主程序是在子程序全部结束后才结束的。
然而有时候,我们会希望提前结束主程序,且需要子程序同步停止。这时,我们我们可以使用如下方法:
- 子程序设置守护程序。其目的是:是主进程退出子进程销毁,不让主进程再等待子进程去执行。我们可以通过
子程序名.daemon = True
为子程序设置守护程序守护主程序。 - 子程序主动停止,不让主程序进入子程序。通过
子程序名.terminate()
。然而这种方式不建议使用。因为其不会清理程序释放资源,可能会导致僵尸程序的出现。
线程
概念
:线程(Thread)是操作系统能够进行运算调度的最小单位,被包含在进程(Process)之中,是进程中的实际运作单元。如果将进程比作是马路上的一辆辆汽车,那么进程就是一条条马路,这就是二者之间的关系。
在进程中会默认有一个线程用来执行程序, 这个线程称之为主线程。在进程创建一个新的线程,这个就是子线程。
作用
:
- 提高程序并发性
- 多任务并行处理:单个进程可以创建多个线程,每个线程独立执行不同任务。
- 提升资源利用效率
- 共享进程资源:线程共享进程的内存空间(代码、堆、全局变量等),通信成本远低于进程间通信(IPC)。
- 轻量级切换:线程的创建、销毁和上下文切换比进程更高效(资源开销更小)。
- 实现异步和响应性
-避免阻塞主流程:将耗时操作(如I/O、网络请求)交给子线程,主线程保持响应。 - 简化复杂任务设计
- 模块化任务分解:将复杂任务拆分为多个线程协作完成,逻辑更清晰。
- 实时系统与高吞吐量服务
- 实时响应:线程的快速切换适合实时系统(如自动驾驶、工业控制)。
- 高吞吐量:服务端程序(如Nginx、Redis)通过多线程处理海量请求。
多线程的创建
多线程的创建步骤与多进程一致,分为三步:
- 导包
- 创建线程对象
- 启动线程对象
通过代码演示如下:
# 导包
import threading
import time
# 定义函数写代码
def coding():
for i in range(10):
print(f"正在写代码...{i}")
time.sleep(1)
# 定义函数听音乐
def music():
for i in range(10):
print(f"正在听音乐...{i}")
time.sleep(1)
if __name__ == '__main__':
# 创建线程对象
t1 = threading.Thread(target=coding)
t2 = threading.Thread(target=music)
# 启动线程
t1.start()
t2.start()
与多进程不同的是Thread对象在创建时可以有多个参数:线程对象=threading.Thread([group [, target [, name [, args [, kwargs]]]]])
- group:线程组,通常为None
- target:线程对象要执行的函数
- name:线程对象名,默认为Thread-x,x为线程编号
- args:传递给线程对象函数的参数,元组
- kwargs:传递给线程对象函数的参数,字典
多线程注意点
在多线程编程中,我们需要注意以下四点:
- 共享全局变量。
- 父线程会等待所有子线程程结束再结束。(与进程一致,因此此点就不在下文中进行代码演示)
- 线程与线程之间的执行是无序的
- 因为多线程之间共享全局变量,所以可能会导致数据出错
共享全局变量
通过如下这段代码演示线程之间共享全局变量
import time
import threading
# 需求:定义一个全局变量,my_list=[],创建两个线程,一个线程往列表中写入数据,一个线程往列表中读取数据
# 1、定义全局变量
my_list = []
# 2、创建两个线程,分别关联两个函数
# 2.1、定义写数据的函数
def write_data():
for i in range(3):
my_list.append(i)
print("写入数据:%d" % i)
print(f"写入数据完成,数据:{my_list}")
# 2.2、定义读数据的函数
def read_data():
time.sleep(3)
print(f"读取数据,数据:{my_list}")
if __name__ == '__main__':
t1 = threading.Thread(target=write_data)
t2 = threading.Thread(target=read_data)
t1.start()
t2.start()
可以看到,原本为空列表的全局变量my_list通过write_data函数写入数据后,三个线程中读到的值均为[0, 1, 2]。这是因为主线程与所有子线程之间是共享全局变量的。
线程与线程之间的执行是无序的
代码实例演示:
# 导包
import threading
import time
# 定义函数写代码
def coding():
for i in range(5):
print(f"正在写代码...{i}")
time.sleep(1)
# 定义函数听音乐
def music():
for i in range(5):
print(f"正在听音乐...{i}")
time.sleep(1)
if __name__ == '__main__':
# 创建线程对象
t1 = threading.Thread(target=coding)
t2 = threading.Thread(target=music)
# 启动线程
t1.start()
t2.start()
可以看到,运行两次代码,得到的结果是不一样的,因此可以说明线程之间的执行是无序的。
数据保护
由于子线程之间的执行顺序是无序,且共享全局变量的,因此可能会导致数据出错。如何控制线程之间的执行顺序确保数据安全就是本节需要研究的问题。
出错原因分析
出错代码实例:
# 导包
import threading
# 定义全局变量
g_num = 0
# 定义两个函数
# 第一个函数,get_sum,实现对g_num全局变量累加100w次
def get_sum1():
global g_num
for i in range(1000000):
g_num += 1
print(f"get_sum1函数,累加结果为:{g_num}")
# 第二个函数,get_sum,实现对g_num全局变量累加100w次
def get_sum2():
global g_num
for i in range(1000000):
g_num += 1
print(f"get_sum2函数,累加结果为:{g_num}")
if __name__ == '__main__':
t1 = threading.Thread(target=get_sum1)
t2 = threading.Thread(target=get_sum2)
t1.start()
t2.start()
原因分析
:不难看出,若是两个子线程有序执行的话,get_sum2函数输出的值应为:2000000。而实际结果却是get_sum2函数输出的值不到2000000。这是由于在线程运行过程中,两个子进程是同时进行的,因此可能会出现在t1进程修改g_num变量时,t2进程读取g_num变量的值还是原来修改前的值,导致数据丢失,进而最后的结果不到2000000。
解决方案
:在线程运行过程中,对全局变量进行保护,即对全局变量进行加锁。
互斥锁
概念
:对共享数据进行锁定,保证同一时刻只有一个线程去操作。
注意:互斥锁是多个线程一起去抢,抢到锁的线程先执行,没有抢到锁的线程进行等待,等锁使用完释放后,其它等待的线程再去抢这个锁。
互斥锁的创建流程:
- 导包
- 创建锁对象
- 上锁
- 释放锁
互斥锁代码实例:
# 导包
import threading
import time
# 定义全局变量
g_num = 0
# 创建一把互斥锁
lock = threading.Lock()
# 定义两个函数
# 第一个函数,get_sum,实现对g_num全局变量累加100w次
def get_sum1():
lock.acquire_lock()
global g_num
for i in range(1000000):
g_num += 1
print(f"get_sum1函数,累加结果为:{g_num}")
lock.release()
# 第二个函数,get_sum,实现对g_num全局变量累加100w次
def get_sum2():
lock.acquire_lock()
global g_num
for i in range(1000000):
g_num += 1
print(f"get_sum2函数,累加结果为:{g_num}")
lock.release()
if __name__ == '__main__':
t1 = threading.Thread(target=get_sum1)
t2 = threading.Thread(target=get_sum2)
t1.start()
t2.start()
通过输出结果可以看到get_sum2函数是在get_sum1函数结束释放锁后才被执行,因此get_sum1累加值为1000000,get_sum2累加值为2000000。
在线程代码结束后需要记得释放锁,否则会造成死锁线程,导致程序停在该线程无法继续运行。
多线程与多进程的区别
在了解完多线程与多进程的相关知识后,我们就可以对多线程与多进程进行总结了。
不同 | 多进程 | 多线程 |
---|---|---|
全局变量 | 不共享 | 共享 |
资源消耗 | 高 | 低 |
单位 | 是资源分配的基本单位 | 是CPU调度的基本单位 |
是否独立进行 | 是 | 否 |
稳定性 | 高 | 低 |
优缺点分析:
优缺点 | 多进程 | 多线程 |
---|---|---|
优点 | 可以用多核 | 资源开销小 |
缺点 | 资源开销大 | 不能使用多核 |