python进阶系列 - 14讲 进程及线程

47 阅读4分钟

我们有两种方法来实现多任务和加速程序:

  • 通过线程
  • 通过进程

进程

进程是一个程序的实例,例如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++ 中)。 这是 numpyscipy 采用的方案。

小节

本文有一定深度,简单总结下:

  • 进程是资源分配的最小单位,一个程序至少有一个进程。
  • 线程是程序执行的最小单位,一个进程至少有一个线程。

并介绍了Python线程及进程的模块threadingmultiprocessing的用法,分享了GIL锁存在的原因及本身的局限。

接下来的2篇文章,我们将深入介绍Python线程及Python进程的详细使用,这里先抛转引玉。

感谢你的阅读。欢迎大家点赞、收藏、支持!

pythontip 出品,Happy Coding!

公众号: 夸克编程

我们的小目标: 让天下木有难学的Python!