15. 多线程和多进程 — Python 进阶

我们有两种常用的方法来并行运行代码(实现多任务并加快程序速度):通过线程或通过多进程。

进程

进程是程序的一个实例,例如Python解释器。它们彼此独立,并且不共享相同的内存。

关键事实:

  • 一个新进程独立于第一个进程启动
  • 充分利用多个CPU和内核
  • 单独的内存空间
  • 进程之间不共享内存
  • 每个进程一个GIL(全局解释器锁),即避免了GIL限制
  • 非常适合CPU密集型处理
  • 子进程可中断/可终止
  • 启动进程慢于启动线程
  • 更大的内存占用
  • IPC(进程间通信)更加复杂

线程

线程是可以调度执行的进程(也称为“轻量级进程”)中的实体。一个进程可以产生多个线程。主要区别在于,进程中的所有线程共享同一内存。

关键事实:

  • 可以在一个进程中产生多个线程
  • 内存在所有线程之间共享
  • 启动线程比启动进程要快
  • 适用于 I/O 密集型任务
  • 轻量
  • 内存占用少
  • 所有线程使用一个GIL,即线程受GIL限制
  • 由于GIL,多线程处理对CPU密集的任务无效
  • 不可中断/杀死->注意内存泄漏
  • 出现竞态情况的可能性增加

Python中的线程

使用 threading 模块。

注意:由于受CPU限制,以下示例通常不会从多个线程中受益。 它应显示如何使用线程的示例。

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 密集型任务。 通过线程化,程序可以花费时间等待这些设备并同时智能地执行其他任务。

示例:从多个站点下载网站信息。 为每个站点使用一个线程。

多进程

使用 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操作且需要大量计算时间的CPU密集型任务很有用。通过多进程,你可以将数据分成相等的部分,然后在不同的CPU上进行并行计算。

示例:计算从1到1000000的所有数字的平方数。将数字分成相等大小的部分,并对每个子集使用一个过程。

GIL-全局解释器锁

这是一个互斥锁(或锁),仅允许一个线程控制Python解释器。这意味着即使在多线程体系结构中,GIL一次也只允许一个线程执行。

为什么需要它?

之所以需要它,是因为CPython(Python的引用实现)的内存管理不是线程安全的。 Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量跟踪指向该对象的引用数。当此计数达到零时,将释放对象占用的内存。问题在于该引用计数变量需要保护,以防止两个线程同时增大或减小其值的竞争条件。如果发生这种情况,则可能导致从未释放的内存泄漏,或者在仍然存在对该对象的引用的情况下错误地释放了内存。

如何避免GIL

GIL在Python社区中引起很大争议。避免GIL的主要方法是使用多线程而不是线程。另一个(但是很不舒服)的解决方案是避免CPython实现,而使用 JythonIronPython 之类的自由线程Python实现。第三种选择是将应用程序的部分移到二进制扩展模块中,即使用Python作为第三方库的包装器(例如在C / C ++中)。这是 numpyscipy 采取的路径。

GitHub repo: qiwihui/blog

Follow me: @qiwihui

Site: QIWIHUI

View on GitHub