• GIL 死锁 递归锁 event 信号量 线程Queue


    一.介绍

    '''
    定义:
    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解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
    ps:python解释器有很多种  最常见的就是Cpython解释器
    GIL本质也是一把互斥锁:将并发变成串行牺牲效率保证数据的安全 
    用来阻止同一个进程下的多个线程的同时执行(同一个进程内多个线程无法实现并行但是可以实现并发)
        
    GIL的存在是因为CPython解释器的内存管理不是线程安全的

    二.GIL介绍

    GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

    可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

    要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程

    '''
    #验证python test.py只会产生一个进程
    #test.py内容
    import os,time
    print(os.getpid())
    time.sleep(1000)
    '''
    python3 test.py 
    #在windows下
    tasklist |findstr python
    #在linux下
    ps aux |grep python
    
    验证python test.py只会产生一个进程
    View Code

    在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问

    #1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
    例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。
    
    #2 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

    综上:

    如果多个线程的target=work,那么执行流程是

    多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

    解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

    三.研究python中的多线程是否有用

    四个任务 计算密集型的  10s
    单核情况下
        开线程更省资源
    多核情况下
        开进程 10s
        开线程 40s
    
    四个任务 IO密集型的  
    单核情况下
        开线程更节省资源
    多核情况下
        开线程更节省资源
    #计算密集型
    from multiprocessing import Process
    from threading import Thread
    import os,time
    def work():
        res=0
        for i in range(100000000):
            res*=i
    
    
    if __name__ == '__main__':
        l=[]
        print(os.cpu_count())  # 本机为8核
        start=time.time()
        for i in range(6):
            p=Process(target=work) #耗时  7.551327705383301
            # p=Thread(target=work) #耗时 27.445183515548706
            l.append(p)
            p.start()
        for p in l:
            p.join()
        stop=time.time()
        print('run time is %s' %(stop-start))
    from multiprocessing import Process
    from threading import Thread
    import threading
    import os,time
    def work():
        time.sleep(2)
    
    
    if __name__ == '__main__':
        l=[]
        print(os.cpu_count()) #本机为8核
        start=time.time()
        for i in range(4000):
            p=Process(target=work) #耗时9.001083612442017s多,大部分时间耗费在创建进程上
            # p=Thread(target=work) #耗时2.051966667175293s多
            l.append(p)
            p.start()
        for p in l:
            p.join()
        stop=time.time()
        print('run time is %s' %(stop-start))

    四.GIL和普通互斥锁

    from threading import Thread
    import time
    n = 100
    def task():
        global n
        tmp = n
        # time.sleep(1)  
        n = tmp -1
    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(n)
    1.当程序中的time.sleep(1)开启后,代码执行到这的时候会进入阻塞态,这时候GIL锁会被释放,别的线程可以进行抢锁,在睡的这1秒的时间中,开启的100个线程都可以轮一遍,都可以抢到锁,所以每个线程中拿到的tmp都为100,在执行玩睡眠时间后执行后面的代码,所以结果为99
    2.当未开启time.sleep(1)时,开启100个线程,每次只有一个线程可以拿到GIL锁,拿到的线程可以对数据进行操作,别的进程只能等到其释放锁后才能进行抢锁,之后再去执行代码,所以结果为0

    五.死锁

    所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁

    from threading import Thread,Lock
    import time
    mutexA=Lock()
    mutexB=Lock()
    
    class MyThread(Thread):
        def run(self):
            self.func1()
            self.func2()
        def func1(self):
            mutexA.acquire()
            print('33[41m%s 拿到A锁33[0m' %self.name)
    
            mutexB.acquire()
            print('33[42m%s 拿到B锁33[0m' %self.name)
            mutexB.release()
    
            mutexA.release()
    
        def func2(self):
            mutexB.acquire()
            print('33[43m%s 拿到B锁33[0m' %self.name)
            time.sleep(2)
    
            mutexA.acquire()
            print('33[44m%s 拿到A锁33[0m' %self.name)
            mutexA.release()
    
            mutexB.release()
    
    if __name__ == '__main__':
        for i in range(10):
            t=MyThread()
            t.start()

    六.递归锁

    死锁的解决方法就是递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

    这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

    mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
    from threading import Thread,Lock,RLock
    import time
    mutexA=mutexB=RLock()
    
    
    class MyThread(Thread):
        def run(self):
            self.func1()
            self.func2()
        def func1(self):
            mutexA.acquire()
            print('33[41m%s 拿到A锁33[0m' %self.name)
    
            mutexB.acquire()
            print('33[42m%s 拿到B锁33[0m' %self.name)
            mutexB.release()
    
            mutexA.release()
    
        def func2(self):
            mutexB.acquire()
            print('33[43m%s 拿到B锁33[0m' %self.name)
            time.sleep(2)
    
            mutexA.acquire()
            print('33[44m%s 拿到A锁33[0m' %self.name)
            mutexA.release()
    
            mutexB.release()
    
    if __name__ == '__main__':
        for i in range(10):
            t=MyThread()
            t.start()
    递归所应用

    七.信号量(Semaphore)

    互斥锁:相当于一个厕所,只有一个坑位
    信号量:相当于一个公共厕所,其中有多个坑位
    from threading import Semaphore,Thread
    import time
    import random
    
    
    sm = Semaphore(5)  # 造了一个含有五个的坑位的公共厕所
    
    def task(name):
        sm.acquire()
        print('%s占了一个坑位'%name)
        time.sleep(random.randint(1,3))
        sm.release()
    
    for i in range(10):
        t = Thread(target=task,args=(i,))
        t.start()

    八.event事件

    同进程的一样

    线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

    event.isSet():返回event的状态值;
    
    event.wait():如果 event.isSet()==False将阻塞线程;
    
    event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
    
    event.clear():恢复event的状态值为False。

    from threading import Event,Thread
    import time
    
    # 先生成一个event对象
    e = Event()
    
    
    def light():
        print('红灯正亮着')
        time.sleep(3)
        e.set()  # 发信号
        print('绿灯亮了')
    
    def car(name):
        print('%s正在等红灯'%name)
        e.wait()  # 等待信号
        print('%s加油门飙车了'%name)
    
    t = Thread(target=light)
    t.start()
    
    for i in range(10):
        t = Thread(target=car,args=('伞兵%s'%i,))
        t.start()

    九.线程Queue

    queue队列 :使用import queue,用法与进程Queue一样

    queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

    1.Queue(先进先出)

    import queue
    
    q=queue.Queue()
    q.put('first')
    q.put('second')
    q.put('third')
    
    print(q.get())
    print(q.get())
    print(q.get())
    '''
    结果(先进先出):
    first
    second
    third
    '''

    2.LifoQueue(先进后出,相当于堆栈)

    import queue
    
    q=queue.LifoQueue()
    q.put('first')
    q.put('second')
    q.put('third')
    
    print(q.get())
    print(q.get())
    print(q.get())
    '''
    结果(后进先出):
    third
    second
    first
    '''

    3.PriorityQueue(设置优先级,数字越小级别越高,级别可为负数)

    import queue
    
    q=queue.PriorityQueue()
    #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
    q.put((20,'a'))
    q.put((-10,'b'))
    q.put((30,'c'))
    
    print(q.get())
    print(q.get())
    print(q.get())
    '''
    结果(数字越小优先级越高,优先级高的优先出队):
    (-10, 'b')
    (20, 'a')
    (30, 'c')
    '''
  • 相关阅读:
    Microsoft NNI入门
    【神经网络搜索】Efficient Neural Architecture Search
    Sphinx 快速构建工程文档
    Ubuntu16.04 Cuda11.1 Cudnn8.1 Tensorflow2.4 PyTorch1.7环境配置
    【CV中的Attention机制】ShuffleAttention
    【有趣的NAS】NAS-RL(ICLR2017)
    分类器
    将url下载到本地
    缓存管理器
    大数据数据结构-分类模型
  • 原文地址:https://www.cnblogs.com/z929chongzi/p/11352401.html
Copyright © 2020-2023  润新知