• 什么是GIL锁,有什么作用?python的垃圾回收机制是什么样的?解释为什么计算密集型用多进程,io密集型用多线程。


    1 什么是gil锁,有什么作用?
    2 python的垃圾回收机制是什么样的?
    3 解释为什么计算密集型用多进程,io密集型用多线程。
    什么是I/O

    1 什么是gil锁,有什么作用?

    1 GIL:Global Interpreter Lock又称全局解释器锁。本质就是一个互斥锁,
    2 保证了cpython进程中得每个线程必须获得这把锁才能执行,不获得不能执行
    3 使得在同一进程内任何时刻仅有一个线程在执行
    4 gil锁只针对于cpython解释器----》
    	JPython
    	PyPy
    	CPython
    
    ***作用:***
    	1 保护Python对象免受多线程并发访问的破坏。
    	2 确保在多线程环境中只有一个线程执行Python字节码。
    	3 GIL的存在使得在CPU密集型任务中,Python的多线程并不能充分发挥多核CPU的优势。
    	  因为只有一个线程能够执行字节码,其他线程会被阻塞。
    
    ***为什么要有gil锁?***
    	python是动态强类型语言,因为有垃圾回收机制,如果同一个进程下有多个线程同时在执行,
    	垃圾回收是垃圾回收线程【同一个进程下变量是共享的】,该线程做垃圾回收时,如果其他线程在运行,
    	就可能会出并发安全的问题【数据安全的问题】,由于当时,只有单核cup【即便开启多线程,同一时刻,
    	也只有一个线程在运行】,作者就强行做了一个GIL锁,保证在一个进程内,同一时刻只有一个线程执行,
    	目的是为了防止垃圾回收线程做垃圾回收时,出现数据紊乱问题,所以加了gil锁
    	
    	**垃圾回收**是垃圾回收线程,它在执行的时候,其他线程是不能执行的,而限于当时的条件,
    		只有单核cpu,所以作者直接做了个GIL锁,保证一个进程内同一时刻只有一个线程在执行。
    
    
    1. **引用计数:** Python 中确实使用引用计数为主要的内存管理机制。每个对象都有一个引用计数,
    	当引用计数为 0 时,对象的内存会被释放。引用计数是线程安全的,但在多线程环境下,
    	其增减并不是原子操作,因此会存在一些复杂的情况。GIL(Global Interpreter Lock)
    	只是确保在同一时刻只有一个线程执行 Python 字节码,而不是在 C 代码层面保护引用计数的操作。
    
    2. **标记清除和隔代回收:** 当引用计数无法解决循环引用等情况时,Python 使用标记清除和隔代回收
    	来解决这些问题。这是针对一些特殊情况的辅助机制。
    
    3. **GIL:** 全局解释器锁(Global Interpreter Lock)是为了在 CPython 解释器中保护内存管理的
    	机制。它确保在同一时刻只有一个线程能够执行 Python 字节码。GIL 并不是 Python 语言本身的一部
    	分,而是 CPython 解释器的特定实现。对于 CPU 密集型任务,GIL 可能会成为性能瓶颈。
    
    4. **多线程和引用计数的问题:** 多线程并不会导致引用计数的问题。引用计数是线程安全的,因为它是
    	CPython 解释器的内部实现。在多线程环境下,GIL 的存在确保了只有一个线程执行 Python 字节码,
    	这意味着对于引用计数的增减操作是原子的。不同线程对同一资源的引用计数操作是协同的,不会导致不
    	一致的情况。
    
    总体而言,引用计数是 Python 内存管理的核心,但 GIL 和其他机制确实为特定情况提供了帮助。
    在多线程环境下,GIL 的存在可能对并行性能产生一些限制,特别是对于 CPU 密集型任务。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    import threading
    
    # 共享变量
    counter = 0
    
    def count_up():
        global counter
        for _ in range(1000000):
            counter += 1
    
    def count_down():
        global counter
        for _ in range(1000000):
            counter -= 1
    
    # 创建两个线程分别执行计数操作
    t1 = threading.Thread(target=count_up)
    t2 = threading.Thread(target=count_down)
    
    # 启动线程
    t1 .start()
    t2.start()
    
    # 等待两个线程执行完成
    t1 .join()
    t2.join()
    
    print("Counter:", counter)
    
    在上述代码中,我们有两个线程,一个递增 counter,一个递减 counter。
    理论上,counter 的最终值应该是 0。但是由于GIL的存在,多线程并发执行时,由于GIL的保护,
    实际上可能并不会得到正确的结果。在这个例子中,counter 的最终值可能不是 0,
    因为两个线程在修改 counter 时可能会发生竞争条件。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2 python的垃圾回收机制是什么样的?

    1 参考文章:
    	https://www.jianshu.com/p/52ab26890114
    	
    2 什么是垃圾回收?
    	编程语言在运行过程中会定义变量--->申请了内存空间---》后期变量不用了---》这个内存空间应该
    	释放掉,有些编程语言,这个操作,需要程序员自己做(c),像java,python,go这些语言,
    	都自带垃圾回收机制,可以自动回收内存空间,gc机制。
    
    3 不同语言垃圾回收的方式是不一样的,python是使用如下三种方式做gc,以引用计数为主,
    	标记-清除和分代回收两个算法为辅
    	(1)引用计数算法(reference counting):
    	每个对象都有一个引用次数的计数属性,如果对象被引用了,那这个数就会 加1,如果引用被删除,
    	引用计数就会 减1,那么当该对象的引用计数为0时,就说明这个对象没有被使用,垃圾回收线程就会
    	把它回收掉,释放内存。
    	   -有问题:循环引用问题---》回收不了
    	   
    	(2) 标记-清除算法(Mark and Sweep)-解决引用计数无法回收循环引用的问题
    	   对象之间通过引用连在一起,节点就是各个对象,从一个根对象向下找对象,可以到达的标记为
    	   活动对象,不能到达的是非活动对象,而非活动对象就是需要被清除的。
    	
    	(3) 分代回收算法(Generational garbage collector)-分代回收是解决垃圾回收效率问题
    	   算法原理是Python把对象的生命周期分为三代,分别是第0代、第1代、第2代。每一代使用双向链表
    	   来标记这些对象。每一代链表都有总数阈值,当达到阈值的时候就会出发GC回收,将需要清除的
    	   清除掉,不需要清除的移到下一代。以此类推,第2代中的对象存活周期最长的对象。
    
       
    注意:python垃圾回收最核心是:
    	引用计数----》标记清除解决引用计数的循环引用问题---》分代回收解决垃圾回收的效率问题。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    
    import gc
    
    # 创建一个循环引用的对象
    class CircularReference:
        def __init__(self):
            self.circular_ref = None
    
    # 创建循环引用
    obj1 = CircularReference()
    obj2 = CircularReference()
    obj1.circular_ref = obj2
    obj2.circular_ref = obj1
    
    # 手动断开引用,使引用计数变为零
    obj1 = None
    obj2 = None
    
    # 手动触发垃圾回收
    gc.collect()
    
    # 由于循环引用,垃圾回收器会将它们回收
    print(gc.garbage)
    
    
    在上述代码中,`CircularReference` 类创建了两个对象 `obj1` 和 `obj2`,
    它们相互引用形成了循环引用。当手动断开对 `obj1` 和 `obj2` 的引用后,手动调用 `gc.collect()` 
    来触发垃圾回收。垃圾回收器会检测到这个循环引用并将其回收。回收后,`gc.garbage` 列表中将包含被
    回收的对象,我们可以通过查看这个列表来确认回收是否成功。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    3 解释为什么计算密集型用多进程,io密集型用多线程。

    ---》只针对于cpython解释器(其他语言,都开多线程即可,能够利用多核优势)
    	-计算是消耗cpu的:代码执行,算术,for都是计算
        -io不消耗cpu:打开文件,写入文件,网络操作都是io
        	-如果遇到io,该线程会释放cpu的执行权限,cpu转而去执行别的线程
    
        -由于python有gil锁,开启多条线程,同一时刻,只能有一条线程在执行
        -如果是计算密集型,开了多线程,同一时刻,只有一个线程在执行
        -多核cpu,就会浪费多核优势
        -如果是计算密集型,我们希望,多个核(cpu),都干活,同一个进程下绕不过gil锁
        -所以我们开启多进程,gil锁只能锁住某个进程中得线程,开启多个进程,就能利用多核优势
        
        
        -io密集型---》只要遇到io,就会释放cpu执行权限
        -进程内开了多个io线程,线程多半都在等待,开启多进程是不能提高效率的,反而开启进程很耗费资源,所以使用多线程即可
        
        
        
        
        
    # web 开发,io多(数据库查数据就是io),开启多线程,明显提高执行效率
    	-uwsgi:进程线程架构---》进程4---》进程下再开线程
        -同一时刻,能允许多少人同时访问---》取决于---4*线程数
        -同一时刻,一个线程只能处理一个请求
        
        
        -异步框架,提高并发量	
        	-一个线程可以处理多个请求
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    什么是I/O

    
    I/O,即输入/输出,是指计算机与外部世界进行交互的过程。在计算机科学中,
    I/O 操作是与外部设备(如磁盘、网络、显示器、键盘等)进行数据交换的一种机制。
    I/O 操作可以分为两大类:
    
    阻塞 I/O(Blocking I/O): 在进行 I/O 操作时,程序会被阻塞(暂停执行),直到 I/O 操作完成。
    这意味着程序将等待数据准备好或者数据传输完成,期间不能执行其他任务。
    
    非阻塞 I/O(Non-blocking I/O): 在进行 I/O 操作时,程序不会被阻塞,而是可以继续执行其他任务。
    程序需要通过轮询或回调等机制来检查 I/O 操作是否完成。
    
    I/O 操作是计算机程序中常见的操作,因为程序通常需要与外部设备或其他程序进行数据交互。
    以下是一些关键概念,有助于理解 I/O:
    
    文件 I/O: 涉及到文件的读取和写入操作。这是最基本的 I/O 操作,通常通过文件描述符或文件句柄进行。
    网络 I/O: 通过网络进行数据传输,包括套接字编程和网络协议的使用。
    设备 I/O: 涉及到硬件设备的输入和输出,如打印机、显示器、键盘等。
    同步 I/O: 程序在进行 I/O 操作时会被阻塞,直到 I/O 完成。
    异步 I/O: 程序在进行 I/O 操作时可以继续执行其他任务,通过回调或事件通知来处理 I/O 完成。
    缓冲 I/O 和直接 I/O: 缓冲 I/O 使用缓冲区进行数据传输,而直接 I/O 直接在用户内存和设备之间传输数据。
    
    理解 I/O 对于编写高性能和响应性良好的程序至关重要。在处理 I/O 时,需要考虑到数据的传输效率、
    错误处理、并发性等方面的问题。异步 I/O 操作常用于构建高性能的、能够同时处理多个任务的应用程序。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    计算密集型任务使用多进程

    import multiprocessing
    
    def calculate_square(numbers):
        result = []
        for number in numbers:
            result.append(number * number)
        print("Result (in process):", result)
    
    if __name__ == "__main__":
        numbers = list(range(1, 6))
        
        # 使用多进程
        process = multiprocessing.Process(target=calculate_square, args=(numbers,))
        process.start()
        process.join()
    
        print("Main process continues...")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    I/O密集型任务使用多线程

    import threading
    import time
    
    def simulate_io_operation():
        print("Start I/O operation...")
        time.sleep(2)  # 模拟I/O操作,比如文件读写或网络请求
        print("I/O operation completed.")
    
    if __name__ == "__main__":
        # 使用多线程
        t1= threading.Thread(target=simulate_io_operation)
        t2= threading.Thread(target=simulate_io_operation)
    
        t1.start()
        t2.start()
    
        t1.join()
        t2.join()
    
        print("Main thread continues...")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这两个例子中,计算密集型任务使用了多进程,而I/O密集型任务使用了多线程。这是因为计算密集型任务中的 GIL 限制了多线程的效果,而I/O密集型任务中可以充分利用多线程的并发性。

  • 相关阅读:
    Linux驱动开发——USB设备驱动
    阿里P9整理分享的亿级流量Java高并发与网络编程实战PDF
    腾讯云前端实习一面
    Jetpack compose比xml优秀的地方
    Google Earth Engine(GEE)——下载2020-2021年的NDBI
    java串口通讯开发rxtxSerial.dll的闪退问题解决
    C++二分算法:水位上升的泳池中游泳
    CSS之字体和层叠样式表引入
    ResNet网络详解
    html中的换行(\n)或回车(\r)符号不起作用的解决办法、br、white、space、pre、line
  • 原文地址:https://blog.csdn.net/weixin_44145338/article/details/134421010