聊聊什么是GIL锁

1. GIL的定义

GIL(Global Interpreter Lock)(全局解释器锁)是CPython解释器中的一种机制,用于确保同一时间只有一个线程可以执行Python字节码。GIL通过在解释器级别上进行互斥锁来实现,这意味着在任何给定的时间点上,只有一个线程可以执行Python字节码和操作Python对象。

Python的GIL是一种特殊的锁,它不是操作系统提供的锁,而是Python解释器提供的锁。当Python解释器创建一个线程时,会自动创建一个与之关联的GIL。当多个线程同时运行时,只有一个线程能够获取GIL,从而执行Python字节码。其它线程则必须等待GIL的释放才能执行。这个机制在保证线程安全的同时,也导致了Python多线程程序的性能问题。

需要注意的是,GIL只影响解释器级别的线程(也称为“内部线程”),而不影响操作系统级别的线程(也称为“外部线程”)。也就是说,在Python程序中使用多个操作系统级别的线程时,这些线程可以并行执行,不受GIL的影响。但是,在同一个解释器中创建的内部线程都受到GIL的限制,只有一个线程能够运行Python代码。

需要注意的是,虽然GIL是Python多线程程序的性能问题之一,但是它并不意味着Python不能使用多线程。对于I/O密集型的任务,Python的多线程模型可以带来性能上的提升。但对于CPU密集型的任务,使用多线程并不能提升性能,反而可能会导致性能下降。此时可以考虑使用多进程或者异步编程等方式来提升性能。

2. GIL的作用机制

GIL的引入是为了解决CPython解释器的线程安全问题。由于CPython的内存管理并不是线程安全的,如果多个线程同时执行Python字节码,可能会导致数据竞争和内存错误。为了解决这个问题,GIL被引入,并确保了同一时间只有一个线程可以执行Python字节码,从而消除了竞争条件。

具体来说,GIL通过在执行Python字节码之前获取并锁定全局解释器锁,从而阻止其他线程执行Python字节码。一旦某个线程获取了GIL,它将独占解释器,并在执行完一定数量的字节码或者时间片后,将GIL释放,使其他线程有机会获取GIL并执行字节码。这个过程在多个线程之间不断重复,以实现多线程的执行。

3. GIL对多线程编程的影响

3.1 CPU密集型任务不会获得真正的并行加速

由于同一时间只有一个线程可以执行Python字节码,对于CPU密集型任务,多线程并不能真正实现并行加速。即使使用多个线程,只有一个线程能够执行字节码,其余线程被GIL阻塞,不能充分利用多核CPU的计算能力。

import threading

def count_up():
    count = 0
    for _ in range(100000000):
        count += 1

t1 = threading.Thread(target=count_up)
t2 = threading.Thread(target=count_up)

t1.start()
t2.start()

t1.join()
t2.join()

上述代码中,t1和t2分别执行count_up函数,该函数进行一亿次的自增操作。然而,在CPython解释器中,由于GIL的存在,实际上只有一个线程能够执行自增操作,因此多线程并不能加速该任务的执行时间。

3.2 I/O密集型任务可以获得一定的并发优势

对于I/O密集型任务,由于线程在等待I/O操作完成时会释放GIL,因此多线程能够发挥一定的并发优势。在等待I/O的过程中,其他线程可以获取GIL并执行Python字节码,从而提高整体程序的执行效率。

import threading
import requests

def fetch_url(url):
    response = requests.get(url)
    print(response.status_code)

urls = [
    'https://www.example1.com',
    'https://www.example2.com',
    'https://www.example3.com',
]

threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

上述代码中,多个线程并发地发起HTTP请求,等待请求完成时会释放GIL,因此可以充分利用CPU资源,并发执行多个网络请求。

3.3 线程间数据共享需要注意同步

由于GIL的存在,多线程在同时访问共享数据时需要注意同步机制,以避免数据竞争和不一致性。

import threading

count = 0

def increment():
    global count
    for _ in range(100000):
        count += 1

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

t1.start()
t2.start()

t1.join()
t2.join()

print("Final count:", count)

在上述代码中,多个线程并发执行自增操作,由于涉及到共享变量count,可能会导致竞争条件。由于GIL的存在,实际上只有一个线程能够执行自增操作,从而可能导致最终的计数结果不正确。

为了避免这种竞争条件,可以使用线程锁(Lock)来进行同步:

import threading

count = 0
lock = threading.Lock()

def increment():
    global count
    for _ in range(100000):
        lock.acquire()
        count += 1
        lock.release()

t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)

t1.start()
t2.start()

t1.join()
t2.join()

print("Final count:", count)

通过引入线程锁,确保每次只有一个线程可以访问和修改共享变量count,从而避免了竞争条件,最终获得正确的计数结果。

4、GIL的准则

1.当前执行线程必须持有GIL
2.当线程遇到 IO的时、时间片到时或遇到阻塞时, 会释放GIL(Python 3.x使用计时器----执行时间达到阈值后,当前线程释放GIL,或Python 2.x,tickets计数达到100。)

5、GIL的优缺点

优点:

线程是非独立的,所以同一进程里线程是数据共享,当各个线程访问数据资源时会出现“竞争”状态,即数据可能会同时被多个线程占用,造成数据混乱,这就是线程的不安全。所以引进了互斥锁,确保某段关键代码、共享数据只能由一个线程从头到尾完整地执行。
缺点:

单个进程下,开启多个线程,无法实现并行,只能实现并发,牺牲执行效率。由于GIL锁的限制,所以多线程不适合计算密集型任务,更适合IO密集型任务。常见IO密集型任务:网络IO(抓取网页数据)、磁盘操作(读写文件)、键盘输入

6、要避免Python的GIL锁带来的影响,可以考虑以下几种方法:

  1. 使用多进程。Python的多进程模型可以避免GIL的限制,多个进程之间可以并行执行Python代码。但是,多进程之间的通信和数据共享需要通过一些额外的手段来实现,如管道、共享内存、套接字等。
  2. 使用第三方扩展模块。一些第三方扩展模块,如NumPy、Pandas等,在执行计算密集型任务时会使用C语言编写的底层库,这些库不会受到GIL的限制。因此,使用这些扩展模块可以提高Python程序的性能。
  3. 使用异步编程。异步编程是一种非阻塞的编程模型,可以在单个线程中执行多个任务,从而避免GIL的限制。Python的异步编程框架有很多,如asyncio、Tornado、Twisted等。
  4. 使用多线程+进程池。多线程可以用来处理I/O密集型任务,而进程池可以用来处理计算密集型任务。将多个线程分配到不同的进程池中可以同时提高I/O密集型和计算密集型任务的处理速度。

需要注意的是,在使用上述方法时,要根据具体的情况来选择合适的方式。例如,在处理大量I/O操作时,使用多进程可能会导致性能下降,因为进程之间的切换开销较大。此时,使用异步编程可能是更好的选择。在处理计算密集型任务时,使用多进程可能是更好的选择,因为进程之间可以并行执行计算密集型任务。

你可能感兴趣的:(python面试指南,前置知识集中营,python进阶,python,GIL)