我们有两种方法来实现多任务和加速程序:
- 通过线程
- 通过进程
进程
进程是一个程序的实例,例如Python解释器。 他们是独立的,并且不共享内存。
关于进程的一些概念:
- 一个新的进程是第一个进程独立的
- 利用多个CPU和多个核心
- 分离内存空间
- 内存不会在进程间共享
- 一个GIL(全局解释器锁)对于每个进程,即使是多个进程也不会有问题。
- 非常适合CPU-bound的处理
- 子进程可以被中断/终止
- 开始进程比开始线程慢
- 大量的内存碎片
- IPC(进程间通信)更复杂
线程
线程是进程中可以调度执行的实体(也称为“轻量级进程”)。 一个进程可以产生多个线程。 主要区别在于进程中的所有线程共享相同的内存。
关于线程的一些概念:
- 一个进程中可以产生多个线程
- 内存在所有线程之间共享
- 启动线程比启动进程快
- 非常适合 I/O 密集型任务
- 轻量级
- 低内存占用
- 所有线程一个 GIL,即线程受 GIL 限制
- 由于 GIL,多线程对 CPU 密集型任务没有影响
- 不可中断/不可杀死
- 小心内存泄漏
- 增加死锁的概率
python线程
使用threading模块。
注意:如果你的程序是CPU-bound,那么这个示例通常不适合多线程。 它只是展示如何使用线程。
from threading import Thread
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
threads = []
num_threads = 10
# 创建线程并为每个线程分配一个函数
for i in range(num_threads):
thread = Thread(target=square_numbers)
threads.append(thread)
# 开始所有线程
for thread in threads:
thread.start()
# 等待所有线程结束
# 主线程阻塞,直到所有线程结束
for thread in threads:
thread.join()
什么时候使用线程
尽管有 GIL,但当你的程序必须与慢速设备(如硬盘驱动器或网络连接)通信时,它对于 I/O 密集型任务很有用。 通过线程,程序可以利用等待这些设备的时间,同时智能地执行其他任务。 比如从多个站点下载网站信息。 为每个站点使用一个线程。
python多进程
用multiprocessing模块。语法和上面的示例类似。
基本用法:
from multiprocessing import Process
import os
def square_numbers():
for i in range(1000):
result = i * i
if __name__ == "__main__":
processes = []
num_processes = os.cpu_count()
for i in range(num_processes):
process = Process(target=square_numbers)
processes.append(process)
for process in processes:
process.start()
for process in processes:
process.join()
什么时候使用多进程
对于 CPU-bound 任务,需要大量的 CPU 操作,并且需要大量的计算时间,那么这个示例适合多进程。 使用多进程,可以将数据分成相等的部分,并且在不同的 CPU 上并行地进行计算。 例如:计算 1 到 1000000 之间的平方数。 分配给每个进程的数据范围是不同的。
GIL 全局解释器锁
这是一个锁,只允许一个线程控制Python解释器。 这意味着,GIL 允许只有一个线程执行,即使是多线程环境。
为什么需要GIL?
因为 CPython 的内存管理不是线程安全的。 Python用户可以通过引用计数来管理内存。 这意味着在 Python 中创建的对象有一个引用计数变量,该变量跟踪指向该对象的引用数量。 当引用计数变为0时,内存占用被释放。 问题是这个引用计数变量需要保护,免受两个线程同时增加或减少其值的竞争条件。 如果发生这种情况,可能会导致内存泄漏而永远不会释放,或者在对该对象的引用仍然存在时错误地释放内存。
如何避免GIL
GIL 在 Python 社区中非常有争议。 避免 GIL 的主要方法是使用多进程而不是线程。 另一个(但不舒服的)解决方案是避免使用 CPython 实现的Python版本,如“Jython”或“IronPython”。
第三种选择是将应用程序的一部分移到二进制扩展模块中,即使用 Python 作为第三方库的包装器(例如在 C/C++ 中)。 这是 numpy
和 scipy
采用的方案。
小节
本文有一定深度,简单总结下:
- 进程是资源分配的最小单位,一个程序至少有一个进程。
- 线程是程序执行的最小单位,一个进程至少有一个线程。
并介绍了Python线程及进程的模块threading
、multiprocessing
的用法,分享了GIL锁存在的原因及本身的局限。
接下来的2篇文章,我们将深入介绍Python线程及Python进程的详细使用,这里先抛转引玉。
感谢你的阅读。欢迎大家点赞、收藏、支持!
pythontip 出品,Happy Coding!
公众号: 夸克编程
我们的小目标: 让天下木有难学的Python!