MST

星途 面试题库

面试题:Python的GIL在不同版本中有哪些改进,对并发编程带来了什么影响?

分析Python不同主要版本(如Python 2.x到Python 3.x 及其子版本)中,全局解释器锁(GIL)的实现方式有哪些改进或变化,这些变化对并发编程的性能、稳定性以及编程模型产生了怎样的影响,并举例说明在特定场景下如何利用这些改进来优化并发程序。
18.7万 热度难度
编程语言Python

知识考点

AI 面试

面试题答案

一键面试

GIL实现方式的改进或变化

  1. Python 2.x:GIL在Python 2.x中,其实现较为简单粗暴。无论什么类型的线程(I/O 密集型或CPU密集型),都需要获取GIL才能执行Python字节码。线程在执行一定数量的字节码指令(通常是100个)后,会释放GIL,让其他线程有机会获取GIL执行。
  2. Python 3.x
    • 时间片机制:在Python 3.2及以后版本,引入了基于时间片的GIL释放机制。线程在获取GIL后,会运行一段固定的时间(默认是15毫秒),然后释放GIL,而不是像Python 2.x那样基于指令数量。这在一定程度上改善了不同类型线程间的调度公平性。
    • 信号处理:Python 3.x在信号处理方面对GIL也有改进。在Python 2.x中,信号处理可能会导致GIL的持有状态出现异常情况,而Python 3.x对此进行了优化,使得信号处理与GIL的交互更加稳定。

对并发编程的影响

  1. 性能
    • CPU密集型任务:在Python 2.x中,由于线程频繁基于指令数释放GIL,对于CPU密集型任务,线程切换开销较大,多核利用率低。在Python 3.x中,基于时间片的释放机制虽然在一定程度上改善了调度,但对于纯CPU密集型任务,依然无法充分利用多核优势,因为同一时间只有一个线程能执行Python字节码。不过,由于时间片机制相对指令数机制更加合理,在多核环境下,整体性能还是有一定提升。
    • I/O密集型任务:无论是Python 2.x还是Python 3.x,GIL对I/O密集型任务影响较小。因为I/O操作通常会释放GIL,让其他线程有机会执行。但Python 3.x的时间片机制使得线程调度在I/O等待期间更加合理,不同I/O线程间切换更高效,从而在I/O密集型场景下性能略有提升。
  2. 稳定性:Python 3.x对GIL与信号处理的优化,使得程序在处理信号时更加稳定,减少了因信号处理导致的GIL相关的死锁或数据不一致问题,提高了程序整体的稳定性。
  3. 编程模型:在编程模型上,虽然GIL的存在限制了Python多线程在CPU密集型任务上的并行执行能力,但无论是Python 2.x还是Python 3.x,多线程对于I/O密集型任务依然是一种简单有效的并发编程模型。然而,由于Python 3.x在GIL调度上的改进,开发者在编写I/O密集型多线程程序时,不用过于担心因指令数调度带来的线程饥饿问题,编程模型相对更加友好。

特定场景下的优化示例

假设我们有一个I/O密集型任务,比如同时下载多个文件:

import threading
import requests


def download_file(url):
    response = requests.get(url)
    file_name = url.split('/')[-1]
    with open(file_name, 'wb') as f:
        f.write(response.content)


urls = [
    'http://example.com/file1',
    'http://example.com/file2',
    'http://example.com/file3'
]

threads = []
for url in urls:
    t = threading.Thread(target=download_file, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

在这个示例中,无论是Python 2.x还是Python 3.x,多线程都能有效利用I/O等待时间来并发执行下载任务。但在Python 3.x中,基于时间片的GIL释放机制,使得不同下载线程间的切换更加合理,能在一定程度上提高整体下载效率,尤其是在有大量I/O任务和线程的情况下。

对于CPU密集型任务,如计算大量数据的平均值,可以使用multiprocessing模块绕过GIL限制:

import multiprocessing


def calculate_mean(data_chunk):
    return sum(data_chunk) / len(data_chunk)


if __name__ == '__main__':
    large_data = list(range(1000000))
    num_processes = multiprocessing.cpu_count()
    chunk_size = len(large_data) // num_processes
    data_chunks = [large_data[i:i + chunk_size] for i in range(0, len(large_data), chunk_size)]

    pool = multiprocessing.Pool(processes=num_processes)
    results = pool.map(calculate_mean, data_chunks)
    pool.close()
    pool.join()

    overall_mean = sum(results) / len(results)
    print(f"Overall mean: {overall_mean}")

通过使用multiprocessing模块,每个进程都有自己独立的Python解释器实例和GIL,从而能充分利用多核CPU资源,提高CPU密集型任务的执行效率。