Python 下的多进程和多线程编程
1.多进程
1. 1 使用多进程完成任务
用到的包:multiprocessing
,以下面的代码为例:
运行的函数示例:
1 | import multiprocessing |
采用常规方式:
1
2
3
4
5
6
7
8if __name__ == '__main__':
start = time.time()
# 常规方式
sing()
dance()
end = time.time()
print(f"花费的时间:{end - start}")在运行时可以发现,是先运行
sing
函数,直到sing
函数完全执行完后,再运行的dance
函数,输出结果为:1
2
3
4
5
6
7sing ...
sing ...
sing ...
dance ...
dance ...
dance ...
花费的时间:3.001246929168701和预想的时间差不多。
采用多进程方式:
1
2
3
4
5
6
7
8
9
10
11
12if __name__ == '__main__':
start = time.time()
# 多进程方式
process1 = multiprocessing.Process(target=sing)
process2 = multiprocessing.Process(target=dance)
process1.start()
process2.start()
process1.join()
process2.join()
end = time.time()
print(f"花费的时间:{end - start}")这里的
Process
类用于创建一个新的子进程。在运行时可以发现,sing
和dance
函数在同时进行,同时观察到输出结果为:1
2
3
4
5
6
7sing ...
dance ...
sing ...
dance ...
sing ...
dance ...
花费的时间:1.5049564838409424基本上是在
1.5s
左右,差不多就是一个函数执行完所需要的时间,多进程执行让两个函数同时开始执行。
1.2 join 方法
注意:在多线程编程中也可以用这个方法!
当我们启动多个进程时,主进程默认不会等待这些子进程结束,而是继续执行自己的代码。通过调用子进程的join()
方法,主进程会暂停执行,直到该子进程运行结束后再继续。
主要作用如下:
确保子进程结束后再继续执行: 主进程在调用join()
时会等待相应的子进程执行完毕,再继续执行后续代码。
同步进程: 如果不使用join()
,主进程可能会在子进程执行完之前就结束,从而导致子进程也被强行终止。
比如说这个例子:
1 | if __name__ == '__main__': |
输出结果是:
1 | sing ... |
要输出主进程中的print("这是一个测试 ... ...")
,需要等1s
的时间,正好是在sing
和dance
函数中的循环体打印两次所需要的时间,可以发现,在到运行的时间时,主进程就直接开始执行了。
现在换成用join()
方法:
1 | if __name__ == '__main__': |
输出结果:
1 | sing ... |
可以看出,是在子进程中的函数执行完后,再执行主进程中的内容。
1.3 进程执行带有参数的任务
运行的函数示例:
1 | import multiprocessing |
一共有两种传参方式:
- args: 元组传参,需要注意只有一个参数时,要用逗号结尾,以及传如参数的顺序。
- kwargs: 字典传参,注意
key
要对应。
1.4 获取子/父进程 PID
1 | import multiprocessing |
输出结果:
1 | 程序的父进程 pid(终端/Shell): 13583 |
1.5 守护进程
运行的函数示例:
1 | import multiprocessing |
在默认情况下,主进程会等待子进程完全执行结束才会退出。
比如这样:
1 | if __name__ == '__main__': |
输出结果:
1 | working ... |
主进程执行到print('任务执行结束!')
这里的时候,已经是执行完了的,但是主进程并没有退出,而是在执行完子进程后才退出的。
现在是设置守护进程的效果:
1 | if __name__ == '__main__': |
让daemon = True
来设置守护进程,主进程退出后,子进程会被直接销毁,不再执行子进程中的代码。
除了用这种方式,还可以通过像Process
传入参数来设置:
1 | if __name__ == '__main__': |
两种方式的效果都是一样的。
2.多线程
2.1 使用多线程完成任务
用到的包:threading
,以下面的代码为例:
运行的函数示例:
1 | import threading |
- 采用多线程方式:输出结果:
1
2
3
4
5
6
7
8
9
10
11
12if __name__ == '__main__':
start = time.time()
thread1 = threading.Thread(target=sing)
thread2 = threading.Thread(target=dance)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
end = time.time()
print(f"花费的时间:{end - start}")使用方式基本上和多进程的一样,只是把包名和函数名换了。1
2
3
4
5
6
7singing ...
dancing ...
singing ...
dancing ...
singing ...
dancing ...
花费的时间:3.0005264282226562
2.2 join 方法
参考多进程的设置方式。
2.3 线程执行带有参数的任务
参考多进程的设置方式,一模一样。
2.4 守护线程
参考多进程的设置方式,一模一样。
2.5 线程之间的执行顺序
线程之间的执行是无序的,以下面这段代码为例:
1 | import threading |
执行结果是这样的:
1 | <Thread(Thread-1 (work), started 140407841228480)> |
从这一个结果上看可能就会得出错误的结论:哪个线程先创建,哪个线程就先执行。但事实上如果我们多执行几次这段代码,就会发现,可能会出现这样的结果:
1 | <Thread(Thread-2 (work), started 130684293220032)> |
可以看出,线程并非是按创建顺序来执行的。这是由于线程调度的随机性,打印顺序是不确定的。
3.总结-多线程和多进程的区别
3.1 工作原理
多线程(Threading):
- 多线程是在 同一个进程的不同线程 中执行代码。线程是轻量级的执行单元,多个线程共享同一进程的内存空间。
- 在 Python 中,线程受制于全局解释器锁(GIL),这个锁保证同一时刻只有一个线程执行 Python 字节码。因此,尽管在多线程环境中可以并发执行 I/O 密集型任务,但对于 CPU 密集型任务,多线程并不会真正实现并行执行。
多进程(Multiprocessing):
- 多进程则是在 多个独立的进程 中运行,每个进程都有自己独立的内存空间和全局解释器锁(GIL)。这意味着不同进程可以真正地在多个 CPU 核心上并行运行,互不干扰。
- 由于进程间不共享内存,因此进程之间的通信需要通过诸如管道(pipes)、队列(queues)等方式进行。
3.2 适用场景
多线程:
- 适合I/O 密集型任务,比如文件读写、网络请求等。在这种情况下,线程在等待 I/O 操作时可以释放 GIL,从而让其他线程运行。
- 不适合 CPU 密集型任务,如大量数学计算,因为 GIL 限制了同一时刻只能有一个线程在执行 Python 代码,无法充分利用多核 CPU。
多进程:
- 适合CPU 密集型任务,例如图像处理、复杂的数学计算、机器学习模型训练等。每个进程都拥有独立的 GIL,因此可以在多核 CPU 上并行执行任务。
- 对于 I/O 密集型任务,也可以使用多进程,不过进程的启动和通信开销较大,性能未必优于多线程。
3.3 性能与资源开销
多线程:
- 线程共享同一进程的内存空间,因此线程之间的数据共享和通信非常快。
- 线程是轻量级的,创建和管理的开销相对较小。
- 由于 GIL 的限制,多线程无法充分利用多核 CPU 进行并行计算,在线程数量较多时,频繁的上下文切换也会影响性能。
多进程:
- 进程之间独立运行,不共享内存,每个进程都有自己的内存空间。这使得进程间的通信开销较大,数据传输需要序列化(例如通过
pickle
)。 - 进程的启动和管理开销较大,因为操作系统需要为每个进程分配内存和资源。
- 多进程可以充分利用多核 CPU,实现真正的并行计算。
- 进程之间独立运行,不共享内存,每个进程都有自己的内存空间。这使得进程间的通信开销较大,数据传输需要序列化(例如通过
3.4 资源共享与数据传递
多线程:
- 线程共享内存空间,因此线程间的变量和数据是共享的。这带来了方便,但也容易引发竞争条件(race conditions)和线程安全问题,必须通过锁(
Lock
)、信号量(Semaphore
)等机制来保护共享资源。
- 线程共享内存空间,因此线程间的变量和数据是共享的。这带来了方便,但也容易引发竞争条件(race conditions)和线程安全问题,必须通过锁(
多进程:
- 进程不共享内存,因此每个进程有自己独立的数据空间。如果需要在进程间传递数据,必须使用进程间通信(IPC)机制,例如队列(
Queue
)、管道(Pipe
)等。进程间通信的开销比线程间通信更大。
- 进程不共享内存,因此每个进程有自己独立的数据空间。如果需要在进程间传递数据,必须使用进程间通信(IPC)机制,例如队列(
3.5 故障隔离
多线程:
- 由于线程共享内存空间,如果一个线程发生异常,可能会影响其他线程,甚至导致整个进程崩溃。
多进程:
- 每个进程是相互独立的,如果一个进程崩溃了,其他进程仍然可以继续运行。因此多进程的故障隔离性更好。
- Title: Python 下的多进程和多线程编程
- Author: loskyertt
- Created at : 2024-09-16 20:38:40
- Updated at : 2024-11-13 03:07:38
- Link: https://redefine.ohevan.com/2024/09/16/Python-多进程和多线程编程/
- License: This work is licensed under CC BY-NC-SA 4.0.