热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

并发编程之GIL

目录GIL什么是GIL锁为什么需要加锁带来的问题如何解决关于性能的讨论

目录

  • GIL
    • 什么是GIL锁
    • 为什么需要加锁
    • 带来的问题
    • 如何解决
    • 关于性能的讨论
      • 计算密集型任务:进程执行更快
      • IO密集型:线程执行更快
    • 自定义锁与GIL的区别

GIL

什么是GIL锁

官方解释:
'''
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple 
native threads from executing Python bytecodes at once. This lock is necessary mainly 
because CPython’s memory management is not thread-safe. (However, since the GIL 
exists, other features have grown to depend on the guarantees that it enforces.)
'''

释义:
在CPython中,这个全局解释器锁,也称为GIL,是一个互斥锁,防止多个线程在同一时间执行Python字节码,这个锁是非常重要的,因为CPython的内存管理非线程安全的,很多其他的特性依赖于GIL,所以即使它影响了程序效率也无法将其直接去除

总结:
在CPython中,GIL会把线程的并行变成串行,导致效率降低

为什么需要加锁

# 线程安全问题具体的表现


# cpython解释器与python程序之间的关系?
python程序本质就是一堆字符串,所有运行一个python程序时,必须要开启一个解释器
但是在一个python程序中解释器只有一个,所有代码都要交给他来解释执行
当有多个线程都要执行代码时,就会产生线程安全问题。
那我们不开启子线程是不是就没有问题了?
答案是:不是。

# Cpython解释器与GC问题
python会自动帮我们处理垃圾,清扫垃圾也是一对代码,也需要开启一个线程来执行
也就是说计算程序没有自己开启线程,内部也有多个线程
GC线程与我们程序中的线程就会产生安全问题
例如:
    线程a要定义一个变量
    步骤:先申请一块空的内存,在吧数据装进去,最后将引用计数加1
    如果在进行到第二部的时候,CPU切换到Gc线程,GC就会把这个值当成垃圾,清理掉
    
总结:为了解决线程安全问题

带来的问题

GIL是一把互斥锁,互斥锁将导致效率低

具体表现是Cpython中,即便开启了多线程,而且CPU也是多核的,却无法并行执行任务。
因为解释器只有一个,同一时间只能有一个任务在执行

如何解决

没办法解决,只能尽可能的避免GIL锁影响我们的效率

1. 使用多进程能够实现并行,从而更好的利用多核CPU
2. 对任务进行区分
    任务可以分为两类
    1. 计算密集型,基本没有IO 大部分时间都在计算,例如人脸识别,图像处理
        由于多线程不能并行,应该使用多进程,将任务分给不同CPU核心
    2. IO密集型,计算任务非常少,大部分时间都在等待IO操作
        由于网络IO速度对比CPU处理速度非常慢,多线程并不会造成太大的影响
        
        对于网络IO密集型,因为有大量客户端连接服务,进程根本开不起来,只能多线程
    

关于性能的讨论

之所以加锁是为了解决线程安全问题
由于有了锁,导致了Cpython中多线程不能并行,只能并发
但是我们不能因此否认python
1. python是一门语言,GIL是Cpython解释器的问题,还有Jpython,pypy
2. 如果是单核CPU  GIL不会造成任何影响
3. 由于目前大多数程序都是基于网络的,网络速度对比CPU非常慢,导致即使多核CPU也无法提高效率
4. 对于IO密集型任务,不会有太大的影响
5. 如果没有这把锁,我们程序猿必须自己来解决安全问题

计算密集型任务:进程执行更快

线程:

from multiprocessing import Process
from threading import  Thread
import time
# 计算密集型任务

def task():
    for i in range(100000000):
        1+1

# 进程
if __name__ == '__main__':
    strat_time = time.time()
    a_list = []
    for i in range(5):
        # p = Process(target=task)
        p = Thread(target=task)
        p.start()
        a_list.append(p)


    for i in a_list:
        i.join()

    print("共耗时:%s"%(time.time()- strat_time))
共耗时:13.50247573852539

进程:

from multiprocessing import Process
from threading import  Thread
import time
# 计算密集型任务

def task():
    for i in range(100000000):
        1+1


# 进程
if __name__ == '__main__':
    strat_time = time.time()
    a_list = []
    for i in range(5):
        p = Process(target=task)
        # p = Thread(target=task)
        p.start()
        a_list.append(p)


    for i in a_list:
        i.join()

    print("共耗时:%s"%(time.time()- strat_time))
共耗时:4.888190746307373

IO密集型:线程执行更快

线程:

from multiprocessing import Process
from threading import  Thread
import time
# 计算密集型任务

def task():
    for i in range(50):
        with open('client_one.py','r',encoding="utf-8")as fr:
            fr.read()


# 进程
if __name__ == '__main__':
    strat_time = time.time()
    a_list = []
    for i in range(5):
        # p = Process(target=task)
        p = Thread(target=task)
        p.start()
        a_list.append(p)


    for i in a_list:
        i.join()

    print("共耗时:%s"%(time.time()- strat_time))
共耗时:0.019989967346191406

进程:

from multiprocessing import Process
from threading import  Thread
import time
# 计算密集型任务

def task():
    for i in range(50):
        with open('client_one.py','r',encoding="utf-8")as fr:
            fr.read()

# 进程
if __name__ == '__main__':
    strat_time = time.time()
    a_list = []
    for i in range(5):
        p = Process(target=task)
        # p = Thread(target=task)
        p.start()
        a_list.append(p)


    for i in a_list:
        i.join()

    print("共耗时:%s"%(time.time()- strat_time))
共耗时:0.33380866050720215

自定义锁与GIL的区别

GIL锁住的是解释器级别的数据,比如对象的引用计数,垃圾分代数据等等,具体参考垃圾回收机制详解

自定义锁的是解释器以外的共享资源,例如:硬盘上的文件,控制台

对于程序中自己定义的数据则没有任何的保护效果,这一点在没有介绍GIL前我们就已经知道了,所以当程序中出现了共享自定义的数据时就要自己加锁,如下例:

from threading import Thread,Lock
import time

a = 0
def task():
    global a
    temp = a
    time.sleep(0.01) 
    a = temp + 1

t1 = Thread(target=task)
t2 = Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
print(a)

过程分析:

  1. 线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL
  2. 线程2获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL
  3. 线程1睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL
  4. 线程2睡醒后获得CPU执行权,并获取GIL执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU并释放GIL,最后a的值也就是1

之所以出现问题是因为两个线程在并发的执行同一段代码,解决方案就是加锁

from threading import Thread,Lock
import time

lock = Lock()
a = 0
def task():
    global a
    lock.acquire()
    temp = a
    time.sleep(0.01)
    a = temp + 1
    lock.release()

t1 = Thread(target=task)
t2 = Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
print(a)

过程分析:

  1. 线程1获得CPU执行权,并获取GIL锁执行代码 ,得到a的值为0后进入睡眠,释放CPU并释放GIL,不释放lock
  2. 线程2获得CPU执行权,并获取GIL锁,尝试获取lock失败,无法执行,释放CPU并释放GIL
  3. 线程1睡醒后获得CPU执行权,并获取GIL继续执行代码 ,将temp的值0+1后赋给a,执行完毕释放CPU释放GIL,释放lock,此时a的值为1
  4. 线程2获得CPU执行权,获取GIL锁,尝试获取lock成功,执行代码,得到a的值为1后进入睡眠,释放CPU并释放GIL,不释放lock
  5. 线程2睡醒后获得CPU执行权,获取GIL继续执行代码 ,将temp的值1+1后赋给a,执行完毕释放CPU释放GIL,释放lock,此时a的值为2

推荐阅读
  • 线程能否先以安全方式获取对象,再进行非安全发布? ... [详细]
  • Python多线程编程技巧与实战应用详解 ... [详细]
  • 本文介绍了如何利用 Delphi 中的 IdTCPServer 和 IdTCPClient 控件实现高效的文件传输。这些控件在默认情况下采用阻塞模式,并且服务器端已经集成了多线程处理,能够支持任意大小的文件传输,无需担心数据包大小的限制。与传统的 ClientSocket 相比,Indy 控件提供了更为简洁和可靠的解决方案,特别适用于开发高性能的网络文件传输应用程序。 ... [详细]
  • Python全局解释器锁(GIL)机制详解
    在Python中,线程是操作系统级别的原生线程。为了确保多线程环境下的内存安全,Python虚拟机引入了全局解释器锁(Global Interpreter Lock,简称GIL)。GIL是一种互斥锁,用于保护对解释器状态的访问,防止多个线程同时执行字节码。尽管GIL有助于简化内存管理,但它也限制了多核处理器上多线程程序的并行性能。本文将深入探讨GIL的工作原理及其对Python多线程编程的影响。 ... [详细]
  • 深入解析C语言中结构体的内存对齐机制及其优化方法
    为了提高CPU访问效率,C语言中的结构体成员在内存中遵循特定的对齐规则。本文详细解析了这些对齐机制,并探讨了如何通过合理的布局和编译器选项来优化结构体的内存使用,从而提升程序性能。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • 本文介绍了如何利用ObjectMapper实现JSON与JavaBean之间的高效转换。ObjectMapper是Jackson库的核心组件,能够便捷地将Java对象序列化为JSON格式,并支持从JSON、XML以及文件等多种数据源反序列化为Java对象。此外,还探讨了在实际应用中如何优化转换性能,以提升系统整体效率。 ... [详细]
  • 本文介绍了如何在iOS平台上使用GLSL着色器将YV12格式的视频帧数据转换为RGB格式,并展示了转换后的图像效果。通过详细的技术实现步骤和代码示例,读者可以轻松掌握这一过程,适用于需要进行视频处理的应用开发。 ... [详细]
  • 并发编程入门:初探多任务处理技术
    并发编程入门:探索多任务处理技术并发编程是指在单个处理器上高效地管理多个任务的执行过程。其核心在于通过合理分配和协调任务,提高系统的整体性能。主要应用场景包括:1) 将复杂任务分解为多个子任务,并分配给不同的线程,实现并行处理;2) 通过同步机制确保线程间协调一致,避免资源竞争和数据不一致问题。此外,理解并发编程还涉及锁机制、线程池和异步编程等关键技术。 ... [详细]
  • 在Python多进程编程中,`multiprocessing`模块是不可或缺的工具。本文详细探讨了该模块在多进程管理中的核心原理,并通过实际代码示例进行了深入分析。文章不仅总结了常见的多进程编程技巧,还提供了解决常见问题的实用方法,帮助读者更好地理解和应用多进程编程技术。 ... [详细]
  • 在Python网络编程中,多线程技术的应用与优化是提升系统性能的关键。线程作为操作系统调度的基本单位,其主要功能是在进程内共享内存空间和资源,实现并行处理任务。当一个进程启动时,操作系统会为其分配内存空间,加载必要的资源和数据,并调度CPU进行执行。每个进程都拥有独立的地址空间,而线程则在此基础上进一步细化了任务的并行处理能力。通过合理设计和优化多线程程序,可以显著提高网络应用的响应速度和处理效率。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • 在处理遗留数据库的映射时,反向工程是一个重要的初始步骤。由于实体模式已经在数据库系统中存在,Hibernate 提供了自动化工具来简化这一过程,帮助开发人员快速生成持久化类和映射文件。通过反向工程,可以显著提高开发效率并减少手动配置的错误。此外,该工具还支持对现有数据库结构进行分析,自动生成符合 Hibernate 规范的配置文件,从而加速项目的启动和开发周期。 ... [详细]
author-avatar
Huiying_Liu
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有