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)定义了一个类 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)定义了一个计算第 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)定义了一个计算第 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密集任务使用多线程。

感谢您花时间阅读文章
关注公众号不迷路

你可能感兴趣的:(编程语言,-,python,python,开发语言)