首页 > 其他分享 >对GIL锁的理解

对GIL锁的理解

时间:2024-01-20 19:11:59浏览次数:28  
标签:tickets 余票 购票 用户 理解 线程 GIL

对GIL锁的理解

【1】介绍

  • 在 Python 中,GIL 或全局解释器锁(Global Interpreter Lock)是一个机制,用于限制 Python 解释器在多线程环境中同时执行多个线程的能力。这是 Python 核心解释器(CPython)中的一个重要部分,它的存在主要是为了简化 CPython 在内存管理上的操作,特别是为了避免与垃圾回收机制相关的复杂性。

【2】为什么会有GIL锁

  • CPython 使用引用计数作为其主要的内存管理技术,这意味着对象一旦没有引用指向它们时就会被立即清除。如果多个线程能够同时执行 Python 字节码,那么在修改任何对象的引用计数时就可能出现竞争条件。例如,两个线程可能同时增加或减少同一个对象的引用计数,导致引用计数被错误地更新。为了避免这种情况,Python 的设计者引入了 GIL。

  • 如果没有这个GIL锁,那么就可能会有一种极限情况,当其中一个线程刚刚申请到内存空间里面的18,还没有来得及绑定给age,这时候垃圾回收机制也执行了,发现居然没有引用计数,直接就把18交给解释器回收掉了。

【3】GIL锁的影响

  • GIL的一个重要影响就是他限制了多线程程序在多核处理器上的并行效率,因为有这个GIL,python在同一时刻,一个进程内只能有一个线程运行,这样就无法发挥多核处理器的优势。这也同时意味着在计算密集型任务上,多线程的效率并不高,因为多线程并不能真正的并行。
  • 然而,在I/O密集型任务中,多线程依旧是非常有效,因为当一个线程进入IO阻塞时,GIL锁会自动释放,是的其他线程可以正常执行。因此,在 I/O 密集型的应用程序中,多线程可以显著提高程序的总体性能和响应性。

【4】如何应对GIL的限制

  • 多进程: 使用 multiprocessing 模块可以创建多个进程,每个进程都有自己的 Python 解释器和内存空间,因此不会受到 GIL 的限制。这对于 CPU 密集型任务非常有效。
  • 其他解释器: 考虑使用不受 GIL 限制的 Python 解释器,如 Jython(基于 Java 虚拟机)或 IronPython(基于 .NET 框架)。
  • 使用 C 扩展: 编写或使用 C/C++ 扩展可以执行 CPU 密集型任务,因为 C/C++ 代码不受 GIL 的限制。
  • 并发库: 使用如 asyncio(用于异步编程)这样的库可以提高 I/O 密集型任务的效率

【5】为什么有了GIL锁,还需要互斥锁?(面试题)

  • 我对此的理解是,大多数时候,我们的传输都是由I/O延迟的,比如说网络延迟就是不可避免的,所以当在传输时,线程遇到了I/O阻塞时就会释放GIL锁,其他线程也是会经历这个过程,所以线程还是可以相对并行的运行,就容易导致读取同一份数据时造成数据错乱。
  • 总的来说,GIL 虽然提供了一定程度的线程安全保护,但它主要是为了简化内存管理和避免某些类型的竞争条件。GIL 并不是用于同步线程间对共享数据的访问。因此,当涉及到共享数据和保证线程之间操作顺序的一致性时,互斥锁仍然是必不可少的。

【6】例子

  • 这里我用一个简易的模拟买票案例说明
from threading import Thread

tickets = 5


def task(i):
    global tickets
    if tickets < 1:
        print(f'用户{i}购票失败:>>>>余票不足')
    else:
        tickets -= 1
        print(f'用户{i}购票成功')


def task_thread():
    t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]

    for t in t_list:
        t.start()


if __name__ == '__main__':
    task_thread()
'''
用户1购票成功
用户2购票成功
用户3购票成功
用户4购票成功
用户5购票成功
用户6购票失败:>>>>余票不足
用户7购票失败:>>>>余票不足
用户8购票失败:>>>>余票不足
用户9购票失败:>>>>余票不足
用户10购票失败:>>>>余票不足

'''
  • 可以看到在这段代码中我并没有加上互斥锁,但是多个线程之间几乎是同时访问tickets这个数据,但是输出的结果却满足我的要求,当票没了,后面的顾客就无法在购票,并没有造成数据错乱。
  • 这就印证了GIL锁的存在,当然这是在理想情况下,也就是没有IO阻塞的情况下。
  • 当我给程序加上那么一点的阻塞,再看看效果
from threading import Thread
import time
tickets = 5


def task(i):
    global tickets
    if tickets < 1:
        time.sleep(0.05)
        print(f'用户{i}购票失败:>>>>余票不足')
    else:
        time.sleep(0.05)
        tickets -= 1
        print(f'用户{i}购票成功')


def task_thread():
    t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]

    for t in t_list:
        t.start()


if __name__ == '__main__':
    task_thread()
'''
用户6购票成功
用户3购票成功
用户2购票成功
用户7购票成功
用户4购票成功
用户1购票成功
用户5购票成功
用户9购票成功
用户10购票成功
用户8购票成功
进程已结束,退出代码0
'''
  • 可以看到,当我在买票环节上加上了0.05的延迟,输出的结果就达不到我的预期了,明明只有5张票,却10个人都买到了票
  • 说明GIL在遇到IO阻塞时就会自动释放锁,这时候就需要我们自己加上互斥锁以保证程序的正常运行了。
from threading import Thread, Lock
import time

tickets = 5
mutex = Lock()


def task(i):
    global tickets
    mutex.acquire()
    if tickets < 1:
        time.sleep(0.05)
        print(f'用户{i}购票失败:>>>>余票不足')
    else:
        time.sleep(0.05)
        tickets -= 1
        print(f'用户{i}购票成功')
    mutex.release()

def task_thread():
    t_list = [Thread(target=task, args=(i,)) for i in range(1, 11)]

    for t in t_list:
        t.start()


if __name__ == '__main__':
    task_thread()
'''
用户1购票成功
用户2购票成功
用户3购票成功
用户4购票成功
用户5购票成功
用户6购票失败:>>>>余票不足
用户7购票失败:>>>>余票不足
用户8购票失败:>>>>余票不足
用户9购票失败:>>>>余票不足
用户10购票失败:>>>>余票不足
进程已结束,退出代码0
'''
  • 所以我认为在后续的编程中,该加锁还得加锁

标签:tickets,余票,购票,用户,理解,线程,GIL
From: https://www.cnblogs.com/Hqqqq/p/17976993

相关文章

  • 对esm模块import理解
    //模块a.jsexportleta=1;exportfunctionaddA(){a++;}//index.jsimpot{a,addA}from'./a.js';console.log('a=',a);//1addA();console.log('a=',a);//2//other.jsimpot{a,addA}from'./a.js'......
  • 深度理解 Spring 动态数据源切换是如何实现的
    更新(不是必读,只为了帮助读者更好的理解执行过程)2022-11-16结合事务TransactionInterceptor的执行,剖析数据源是如何切换的详细分析为什么,切面要设置@Order(-9999)属性针对点一回答如下在SpringBoot项目启动的时候,会去扫描所有配置类,生成一个个的Bean,被@Transaction标记的......
  • 0-1背包问题 初理解
    第一次做这题确实没什么思路,看了卡哥的视频也是似懂非懂,现在整理一下。首先明确变量有哪些,物品种类,单个物品重量,单个物品价值,背包的最大容量容量;这些变量该如何融入递推公式中呢?先明确题目所求的是什么,在背包容纳范围下的价值最大。为了求容量空间为N的价值最大,可以推想容量空......
  • 使用最小花费爬楼梯 动态规划初理解
    该题是动态规划入门程度,但最开始做的时候还是无从下手。我觉得卡哥给的步骤很重要:确定dp数组(dptable)以及下标的含义确定递推公式dp数组如何初始化确定遍历顺序举例推导dp数组首先明确dp数组(dptable)以及下标的含义很重要,最开始做这道题的时候,设了dp但不知道是代表什么。......
  • 003 Salesforceの基本的なキーワードを理解する
    0.はじめにこれから業務でSalesforceを使い始める人は事前に基本的なキーワードを確認することで運用開始に役立てていただければとおもいます。またすでにSalesforceを運用している人にとっても改めて見直すことにより利用の幅を広げていただけるかもしれません。1.主な製品S......
  • 性能调优:深入理解 FastAPI 并发请求处理
    在当今的数字化世界中,网络用户对于高速响应和持续连接的诉求日益显著。这促使了基于Python构建的FastAPI一、开始使用FastAPI在开始之前,请先确认已经安装了FastAPI。可以通过以下pip命令进行安装:1pipinstallfastapi建设一个基础的FastAPI应用来实例化并发请求处理非常直......
  • 深入理解JavaScript堆栈、事件循环、执行上下文、作用域以及闭包
    合集-JavaScript进阶系列(5) 1.JavaScriptthis绑定详解01-092.JavaScriptapply、call、bind函数详解01-093.JavaScriptforEach方法跳出循环01-024.深入理解JavaScript堆栈、事件循环、执行上下文和作用域以及闭包01-105.JavaScript到底应不应该加分号?JavaScript自......
  • 深入理解Java中的ThreadLocal
    第1章:引言大家好,我是小黑。今天咱们来聊聊ThreadLocal。首先,让咱们先搞清楚,ThreadLocal是个什么玩意儿。简单说,ThreadLocal可以让咱们在每个线程中创建一个变量的“私有副本”。这就意味着,每个线程都可以独立地改变自己的副本,而不会影响其他线程。这就像是每个人都有自己的笔记......
  • 对线程的理解
    【一】什么是线程线程可以被看作是在程序内部的一个独立的任务流,它是操作系统能够进行运算调度的最小单位。线程存在于进程之中,可以把进程想象成一个工厂,而线程就像是工厂里的工人。想象你有一个工厂(这个工厂就像一个进程),在这个工厂里有很多工人(这些工人就是线程)。这些工人......
  • 抓包Tcpdump 学习与理解
    常用参数:-w文件名,可以把报文保存到文件;-c数量,可以抓取固定数量的报文,这在流量较高时,可以避免一不小心抓取过多报文;-s长度,可以只抓取每个报文的一定长度,后面我会介绍相关的使用场景;-n,不做地址转换(比如IP地址转换为主机名,port80转换为http);-v/-vv/-vvv,可以打印更加详细......