• Python 并发:全局解释器锁(GIL)及其对多线程的影响


    1. 写在前面

    Python 是一种流行的高级编程语言,以其简单、易用和快速开发而著称。然而,Python 的垃圾回收机制依赖于全局解释器锁(GIL: Global Interpreter Lock),这可能会造成一些限制。本文将探讨 Python 中指针的各个方面,尤其是 GIL 对内存管理、多线程和 CPU 利用率的影响。此外,本文还将提供具体示例来说明其局限性和解决方法。

    公众号: 滑翔的纸飞机

    2 内存管理和全局解释器锁 (GIL)

    Python 使用垃圾回收器来自动管理内存。垃圾回收器通过检测和删除程序不再使用的对象来释放内存。不过,垃圾回收器需要依赖全局解释器锁 (GIL) 才能正常工作。GIL 是一种防止多个线程同时执行 Python 字节码的机制。GIL 是必要的,因为 Python 的内存管理不是线程安全的,这意味着两个线程不能同时访问相同的内存位置,否则会有损坏数据的风险。

    GIL 对内存管理有一些影响。例如,它可以防止垃圾回收器在多个线程中同时运行。因此,在垃圾回收器运行之前,不再使用的对象所占用的内存不会被清除。这可能会导致内存泄漏并降低性能。

    下面的例子可以说明这种限制:

    """
    @Time:2023/11/7 23:10
    @Describe:
    """
    import threading
    
    
    class MyClass:
        def __init__(self):
            self.my_list = []
    
        def add_value(self, value):
            self.my_list.append(value)
    
    
    my_object = MyClass()
    
    
    def add_values():
        for i in range(10000000):
            my_object.add_value(i)
    
    
    thread_1 = threading.Thread(target=add_values)
    thread_2 = threading.Thread(target=add_values)
    
    thread_1.start()
    thread_2.start()
    
    thread_1.join()
    thread_2.join()
    
    print(len(my_object.my_list))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在本例中:
    (1)定义了一个类 MyClass,并包含一个列表属性 my_list;
    (2)add_values() 函数将 1000 万个值添加到 my_list;
    (3)最后,创建该类的一个实例,然后创建两个调用该函数的线程,最后打印长度;

    然而,由于 GIL 的存在,两个线程无法并行运行,程序执行时间并不会减少太多。此外,垃圾回收器可能不会在线程执行期间运行,这意味着添加值所使用的内存可能不会被清除。这可能会导致内存泄漏并降低性能。

    3 多线程和全局解释器锁 (GIL)

    全局解释器锁 (GIL) 也会影响 Python 中的多线程。GIL 的工作原理是为每个变量加锁,并维护一个使用计数器。如果一个线程想访问一个已被另一个线程使用的变量,它必须等到第一个线程释放了该变量。因此,一次只能有一个线程执行 Python 字节码。

    这一限制可能会对严重依赖 CPU 操作的多线程程序产生影响。 例如,如果一个程序有两个执行复杂计算的线程,GIL将阻止它们并行运行,并且该程序将无法从使用多个CPU核心中受益。

    下面的例子可以说明这种限制:

    """
    @Time:2023/11/7 23:19
    @Describe:
    """
    
    import threading
    
    
    def fib(n):
        if n <= 1:
            return n
        else:
            return fib(n - 1) + fib(n - 2)
    
    
    def compute_fib():
        for i in range(30):
            print(fib(i))
    
    
    thread_1 = threading.Thread(target=compute_fib)
    thread_2 = threading.Thread(target=compute_fib)
    
    thread_1.start()
    thread_2.start()
    
    thread_1.join()
    thread_2.join()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在本例中:
    (1)定义了一个计算第 n 个斐波那契数的 fib 函数。
    (2)定义了一个 compute_fib 函数,通过调用 fib 函数来计算前 30 个斐波那契数。
    (3)同时,创建两个调用 compute_fib 函数的线程,然后启动它们。
    (4)最后,我们使用 join 方法等待线程结束。

    然而,由于GIL的原因,两个线程无法并行执行Python字节码。 因此,程序不会从使用多个 CPU 核心中受益,并且执行时间与使用单线程相同。 为了提高CPU 多核利用率,我们可以使用多进程(multiprocessing)。 多进程模块允许我们创建多个并行运行的进程,每个进程都有自己的解释器和内存空间。 这意味着每个进程都可以使用自己的CPU核心来执行Python字节码,进程GIL相互不影响。

    下面的示例说明了如何使用多进程模块计算斐波那契数列:

    import multiprocessing
    
    def fib(n):
    if n <= 1:
    return n
    else:
    return fib(n-1) + fib(n-2)
    
    def compute_fib(start, end):
    for i in range(start, end):
    print(fib(i))
    
    if name == 'main':
    with multiprocessing.Pool(processes=2) as pool:
    pool.starmap(compute_fib, [(0, 15), (15, 30)])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在本例中:

    (1)定义了一个计算第 n 个斐波那契数字的 fib 函数;
    (2)定义了一个 compute_fib 函数,该函数通过调用 fib 函数来计算一系列数字的斐波那契数列。我们使用 multiprocessing.Pool 方法创建一个由两个进程组成的进程池,然后使用 starmap 方法对两个数字范围(0 至 15 和 15 至 30)执行 compute_fib 函数。starmap 方法会将数字范围分配给两个进程,每个进程会计算其分配范围内的斐波那契数列。最后打印出结果;

    通过使用多进程模块,我们可以利用多个 CPU 内核并行执行 Python 字节码,而不会受到 GIL 的影响。这可以大大提高 CPU 的性能。

    4 结论

    Python中的线程需要先获取GIL锁才能继续运行,每一个进程仅有一个GIL,线程在获取到GIL之后执行100字节码或者遇到IO中断时才会释放GIL,这样在CPU密集的任务中,即使有多个CPU,多线程也是不能够利用多个CPU来提高速率,甚至可能会因为竞争GIL导致速率慢于单线程。所以对于CPU密集任务往往使用多进程,IO密集任务使用多线程。

    感谢您花时间阅读文章
    关注公众号不迷路
  • 相关阅读:
    2022-8-6 集合容器
    Mysq优化---mysql安装与配置(Lunix环境)
    【Vue】input框自动聚焦且输入验证码后跳至下一位
    盘点2022年最受欢迎的6大前端框架
    【bug】内存占用过高,当前为[61G]
    高并发内存池项目(C++实战项目)
    Mybatis的Xml映射文件
    代码随想录day55|392.判断子序列|115.不同的子序列|Golang
    重邮803计网概述
    链表(1)
  • 原文地址:https://blog.csdn.net/u011521019/article/details/134300755