Python中的全局解释器锁-GIL

/ 默认分类 / 0 条评论 / 266浏览

一.python中的线程池使用

python支持使用线程池,也就是concurrent.futures.ThreadPoolExecutor。线程池的使用和别的计算机语言例如java等基本一致,只是在线程池的实现方式存在一定的差异。 下面是线程池的测试代码:

from concurrent.futures import ThreadPoolExecutor, as_completed
import random
from datetime import datetime
import math

# 生成一个包含100个随机数字的数组
array = [random.randint(1, 100) for _ in range(100)]


# 定义一个耗时2秒的方法
def simulate_long_task(duration=2):
    start = datetime.now()
    while (datetime.now() - start).seconds < duration:
        # 执行一些计算密集型操作来模拟耗时
        math.factorial(1000)


# 定义一个函数用于累加数组中的部分元素
def partial_sum(sub_array, index):
    start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{start_time}] Task {index} starting with array slice: {sub_array}")

    # 模拟耗时操作
    simulate_long_task()

    result = sum(sub_array)
    end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{end_time}] Task {index} completed with result: {result}")
    return result


# 将数组拆分为5个等分的部分
def split_array(array, num_parts):
    length = len(array)
    part_size = length // num_parts
    return [array[i * part_size:(i + 1) * part_size] for i in range(num_parts)]


# 主函数,用于使用线程池计算数组的累加和
def main():
    num_parts = 5
    sub_arrays = split_array(array, num_parts)

    with ThreadPoolExecutor(max_workers=num_parts) as executor:
        # 提交部分数组的累加任务
        futures = [executor.submit(partial_sum, sub_arrays[i], i) for i in range(num_parts)]

        # 等待所有任务完成并累加结果
        total_sum = 0
        for future in as_completed(futures):
            total_sum += future.result()

    final_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{final_time}] Total sum of array: {total_sum}")


# 运行主函数
if __name__ == "__main__":
    main()

运行结果:

[2024-06-02 15:23:09] Task 0 starting with array slice: [63, 74, 3, 96, 42, 51, 13, 41, 62, 72, 41, 35, 89, 28, 63, 39, 67, 43, 64, 44]
[2024-06-02 15:23:09] Task 1 starting with array slice: [96, 51, 50, 72, 79, 25, 23, 70, 22, 68, 57, 39, 10, 93, 39, 16, 15, 92, 18, 78][2024-06-02 15:23:09] Task 2 starting with array slice: [97, 68, 95, 76, 63, 47, 43, 61, 89, 4, 58, 36, 16, 44, 13, 43, 71, 65, 25, 36]
[2024-06-02 15:23:09] Task 3 starting with array slice: [69, 1, 16, 75, 26, 53, 30, 65, 73, 4, 83, 12, 75, 91, 75, 44, 12, 75, 30, 100]
[2024-06-02 15:23:09] Task 4 starting with array slice: [49, 26, 51, 52, 24, 87, 99, 62, 63, 17, 71, 38, 60, 42, 66, 6, 99, 33, 64, 10]

[2024-06-02 15:23:11] Task 0 completed with result: 1030
[2024-06-02 15:23:11] Task 3 completed with result: 1009[2024-06-02 15:23:11] Task 1 completed with result: 1013
[2024-06-02 15:23:11] Task 4 completed with result: 1019

[2024-06-02 15:23:11] Task 2 completed with result: 1050
[2024-06-02 15:23:11] Total sum of array: 5121

可以看到,将100个数字拆分为5份之后(任务逻辑中加上一个睡眠模拟耗时2s的逻辑),可以放入线程池中执行,所有子任务执行完毕后得到最终的累加结果,只花费了2s的时间,所以这五个任务是并行计算的效果。

二.GIL全局解释器锁

python GIL说明官方文档

全局解释器锁(Global Interpreter Lock,简称 GIL)是 Python 解释器中的一个机制,用于确保在任何给定时间点只有一个线程在执行 Python 字节码。换句话说,GIL 会在解释器级别上对 Python 的多线程代码进行限制,使得在多核处理器上并行执行 Python 代码变得困难。 以下是关于 GIL 的详细解释:

  1. GIL 是什么?
    • GIL 是 Python 解释器中的一个互斥锁,它是用于保护解释器内部数据结构不受并发访问的影响。
    • 在 CPython(即标准的 Python 解释器)中,GIL 是必需的,因为解释器的内存管理并不是线程安全的,如果没有 GIL,可能会导致数据结构损坏和内存泄漏。
  2. 为什么存在 GIL?
    • GIL 的存在是为了简化 CPython 的内存管理模型。它简化了解释器的实现,并且能够更轻松地集成 C 语言库和操作系统线程。
    • 在 CPython 中,全局解释器锁(或GIL)是一个互斥锁,用于保护对 Python 对象的访问,防止多个线程同时执行 Python 字节码。GIL 可防止竞争条件并确保线程安全。简而言之,此互斥锁是必需的,主要是因为 CPython 的内存管理不是线程安全的。
    • GIL 使得 CPython 在单线程环境中能够表现出很好的性能,因为它可以避免多线程竞争带来的开销。
  3. GIL 对多线程程序的影响:
    • GIL 对 I/O 密集型任务的影响相对较小,因为在 I/O 操作时,Python 解释器会释放 GIL,允许其他线程执行。
    • 然而,对于 CPU 密集型任务,GIL 会导致线程之间无法并行执行 Python 字节码,因为只有持有 GIL 的线程才能执行字节码。这意味着即使使用了多线程,也只有一个线程能够在任何给定时刻执行 Python 代码,其他线程只能等待 GIL 的释放。
  4. 为什么 GIL 会影响线程池的并发效率?
    • 在使用线程池时,尽管线程池会管理一组线程来执行任务,但由于 GIL 的存在,一次只有一个线程能够执行 Python 字节码。这意味着在 CPU 密集型任务中,多个线程在竞争执行 Python 代码时,并不能真正完全并行执行,因为它们必须等待 GIL 的释放才能执行。
    • 因此,尽管线程池可以提高 I/O 密集型任务的并发性能,但在 CPU 密集型任务中,由于 GIL 的限制,无法充分利用多核处理器的性能。

总的来说,GIL 的存在是 Python 中一种权衡,它简化了解释器的实现,但在一些情况下会对多线程程序的性能产生一定的影响,特别是在 CPU 密集型任务中。

注意,上面我们说的是,即使在多核cpu环境下,不能实现完全的并行执行效果,但是对于我们上面的程序,实际上是实现了并行执行效果。这是为什么呢? 尽管Python的全局解释器锁(GIL)确实存在,线程池在处理IO密集型任务时不会受到显著影响,主要是因为以下几个原因:

1. GIL和IO密集型任务

GIL主要影响CPU密集型任务,因为它限制了只有一个线程能执行Python字节码。然而,对于IO密集型任务(例如网络请求、文件读写),线程在等待IO操作时会释放GIL,使得其他线程可以继续执行。这种情况下,多线程仍然可以显著提高性能。

2. 模拟耗时操作

在之前的示例中,尽管我们使用了计算密集型操作来模拟耗时操作,但这种操作实际上可能没有显著超出GIL的管理范围。因此,在这种模拟中,虽然GIL存在,但多线程在短时间内快速执行完计算任务,然后释放GIL,使得其他线程能够继续执行。这种情况下,线程池看起来仍然能够并行执行多个任务。

3. 线程池的优势

即便在有GIL的限制下,线程池可以通过快速切换线程来处理多个任务。这种情况下,每个线程在短时间内执行一些任务,然后切换到其他任务,使得多线程可以提高程序的响应性和并发性。尤其是在单个任务本身就耗时较短的情况下,加上线程调度本身的逻辑耗时,并发提升的效率就更加不明显甚至更低。

官方文档:GIL 并不理想,因为它会阻止多线程 CPython 程序在某些情况下充分利用多处理器系统。幸运的是,许多潜在的阻塞或长时间运行的操作(例如 I/O、图像处理和NumPy数字运算)都发生在 GIL_之外_。因此,只有在花费大量时间在 GIL 中解释 CPython 字节码的多线程程序中,GIL 才会成为瓶颈。 不幸的是,自从 GIL 存在以来,其他功能已经越来越依赖于它所强制执行的保证。这使得很难在不破坏许多官方和非官方 Python 包和模块的情况下删除 GIL。 即使GIL不是瓶颈,它也会降低性能。总结链接的幻灯片:系统调用开销很大,尤其是在多核硬件上。两个线程调用一个函数所花的时间可能是单个线程调用该函数两次所花时间的两倍。GIL 可能导致 I/O 密集型线程优先于 CPU 密集型线程进行调度,并阻止信号传递。