• 【0918 | Day 33】线程锁/死锁/递归锁/GIL锁/多进程vs多线程


    线程锁

    例子(线程锁)

    线程锁本质是一个互斥锁,保证数据安全

    不加锁的情况

    from threading import Thread, Lock
    
    x = 0
    mutex = Lock()
    def task():
        global x
        for i in range(100000):
            x += 1
            '''
            
            ==》t1 的 x刚拿到0 保存状态 就被切了
            ==》t2 的 x拿到0 进行+1       1
            ==》t1 又获得运行了  x = 0  +1  1
            
            思考:一共加了几次1? 加了两次1 真实运算出来的数字本来应该+2 实际只+1
            
            因此产生数据安全的问题
            
            '''
    
    if __name__ == '__main__':
        t1 = Thread(target=task)
        t2 = Thread(target=task)
        t3 = Thread(target=task)
        t1.start()
        t2.start()
        t3.start()
    
        t1.join()
        t2.join()
        t3.join()
        print(x)  #结果<=300000(注意只有当range数据较大时才会产生这个问题)
    

    加锁的情况

    from threading import Thread, Lock
    
    x = 0
    mutex = Lock()
    def task():
        global x
        mutex.acquire()
        
        for i in range(100000):
            x += 1
    
        mutex.release()
    
    if __name__ == '__main__':
        t1 = Thread(target=task)
        t2 = Thread(target=task)
        t3 = Thread(target=task)
        t1.start()
        t2.start()
        t3.start()
    
        t1.join()
        t2.join()
        t3.join()
        print(x)  #300000
    

    死锁

    介绍

    死锁的这个概念在很多地方都存在,大概介绍下是怎么产生的

    • A拿了一个苹果

    • B拿了一个香蕉

    • A现在想再拿个香蕉,就在等待B释放这个香蕉

    • B同时想要再拿个苹果,这时候就等待A释放苹果

    • 这样就是陷入了僵局,这就是生活中的死锁

    python中在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

    因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。

    例子(死锁)

    import threading
    import time
    
    lock_apple = threading.Lock()
    lock_banana = threading.Lock()
    
    class MyThread(threading.Thread):
    
        def __init__(self):
            threading.Thread.__init__(self)
    
        def run(self):
            self.fun1()
            self.fun2()
    
        def fun1(self):
    
            lock_apple.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放
    
            print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
    
            lock_banana.acquire()
            print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
            lock_banana.release()
            lock_apple.release()
    
    
        def fun2(self):
    
            lock_banana.acquire()
            print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
            time.sleep(0.1)
    
            lock_apple.acquire()
            print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
            lock_apple.release()
    
            lock_banana.release()
    
    if __name__ == "__main__":
        for i in range(0, 10):  #建立10个线程
            my_thread = MyThread()  #类继承法是python多线程的另外一种实现方式
            my_thread.start()
    
    -----------------------------------我是一条分割线---------------------------------
    
    线程 Thread-1 , 想拿: 苹果--Sun Apr 28 12:21:06 2019
    线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
    线程 Thread-1 , 想拿: 香蕉--Sun Apr 28 12:21:06 2019
    线程 Thread-2 , 想拿: 苹果--Sun Apr 28 12:21:06 2019
    

    上面的代码其实就是描述了苹果和香蕉的故事。大家可以仔细看看过程。下面我们看看执行流程

    • fun1中,线程1先拿了苹果,然后拿了香蕉,然后释放香蕉和苹果,然后再在fun2中又拿了香蕉,sleep 0.1秒。

    • 在线程1的执行过程中,线程2进入了,因为苹果被线程1释放了,线程2这时候获得了苹果,然后想拿香蕉

    • 这时候就出现问题了,线程一拿完香蕉之后想拿苹果,返现苹果被线程2拿到了,线程2拿到苹果执行,想拿香蕉,发现香蕉被线程1持有了

    • 双向等待,出现死锁,代码执行不下去了

    上面就是大概的执行流程和死锁出现的原因。在这种情况下就是在同一线程中多次请求同一资源时候出现的问题。

    递归锁(RLock)

    介绍

    • 为了支持在同一线程中多次请求同一资源,python提供了"递归锁":threading.RLock。

    • RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

    例子(递归锁)

    下面我们用递归锁RLock解决上面的死锁问题:

    import threading
    import time
    
    lock = threading.RLock()  #递归锁
    
    
    class MyThread(threading.Thread):
    
        def __init__(self):
            threading.Thread.__init__(self)
    
        def run(self):
            self.fun1()
            self.fun2()
    
        def fun1(self):
    
            lock.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放
    
            print (f"线程{self.name}, 想拿:'苹果'--{time.ctime()}" )
    
            lock.acquire()
            print (f"线程{self.name}, 想拿: '香蕉'--{time.ctime()}" )
            lock.release()
            lock.release()
    
    
        def fun2(self):
    
            lock.acquire()
            print (f"线程{self.name}, 想拿: '香蕉'--{time.ctime()}" )
            time.sleep(0.1)
    
            lock.acquire()
            print (f"线程{self.name}, 想拿:'苹果'--{time.ctime()}" )
            lock.release()
    
            lock.release()
    
    if __name__ == "__main__":
        for i in range(0, 3):  #建立10个线程
            my_thread = MyThread()  #类继承法是python多线程的另外一种实现方式
            my_thread.start()
    

    线程Thread-1, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
    线程Thread-1, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
    线程Thread-1, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
    线程Thread-1, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
    线程Thread-2, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
    线程Thread-2, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
    线程Thread-2, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
    线程Thread-2, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
    线程Thread-3, 想拿:'苹果'--Wed Sep 18 17:01:30 2019
    线程Thread-3, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
    线程Thread-3, 想拿: '香蕉'--Wed Sep 18 17:01:30 2019
    线程Thread-3, 想拿:'苹果'--Wed Sep 18 17:01:30 2019

    上面我们用一把递归锁,就解决了多个同步锁导致的死锁问题。大家可以把RLock理解为大锁中还有小锁,只有等到内部所有的小锁,都没有了,其他的线程才能进入这个公共资源。

    思考

    如果我们都加锁也就是单线程了,那我们还要开多线程有什么用呢?

    • 这里解释下,在访问共享资源的时候,锁是一定要存在的。

    • 但是我们的代码中,不总是在访问公共资源的,还有一些其他的逻辑可以使用多线程。

    • 所以我们在代码里面加锁的时候,要注意在什么地方加,对性能的影响最小,这个就靠对逻辑的理解了。

    信号量(Semphare)

    介绍

    它控制同一时刻多个线程访问同一个资源的线程数

    • 实例化时,指定使用量。
    • 其内置计数器,锁定时+1, 释放时-1,计数器为0则阻塞。
    • acquire(blocking=True,timeout=None)
    • release()释放锁

    例子(信号量)

    from threading import Thread,currentThread,Semaphore
    import time
    
    
    def task():
        sm.acquire()
        print(f'{currentThread().name} 在执行')
        time.sleep(3)
        sm.release()
    
    sm = Semaphore(4)
    for i in range(12):
        t = Thread(target=task)
        t.start()
    

    Thread-1 在执行
    Thread-2 在执行
    Thread-3 在执行
    Thread-4 在执行

    Thread-5 在执行
    Thread-6 在执行
    Thread-7 在执行
    Thread-8 在执行

    Thread-9 在执行
    Thread-10 在执行
    Thread-11 在执行
    Thread-12 在执行

    GIL(全局解释器锁)

    介绍

    问题一:为什么python在多线程中为什么不能实现真正的并行操作呢(即在多CPU中执行不同的线程)?

    • 这就要提到python中大名鼎鼎GIL,那什么是GIL?

    • GIL:全局解释器锁 无论你启多少个线程,你有多少个CPU, Python在执行的时候只会的在同一时刻只允许一个线程(线程之间有竞争)拿到GIL在一个CPU上运行。

    • 当线程遇到IO等待或到达者轮询时间的时候,CPU会切换,把CPU的时间片让给其他线程执行.

    • CPU切换需要消耗时间和资源,所以计算密集型的功能(比如加减乘除)不适合多线程,因为CPU线程切换太多,IO密集型比较适合多线程。

    问题二:为什么要有GIL锁?

    • 因为cpython自带的垃圾回收机制不是线程安全的,一旦变量的引用计数为0,就会被回收。此时GIL锁就是与万恶的垃圾回收机制相抗衡,不让它这么块就过来抢我们暂时无家可归的小可爱(变量)!!!

    • 不过呢,GIL锁也导致了同一个进程同一时间只能运行一个线程,无法利用到多核优势。

    分析:我们有四个任务需要处理,处理方式肯定是要玩出并发的效果

    解决方案可以是:

    • 方案一:开启四个进程

    • 方案二:一个进程下,开启四个线程

    例子(任务)

    io密集型

    '''采用多进程计时情况'''
    
    def work1():
        x = 1+1
        time.sleep(5)
    
    
    if __name__ == '__main__':
        t_list = []
        start = time.time()
        for i in range(4):
            t = Process(target=work1)
            t_list.append(t)
            t.start()
        for t in t_list:
            t.join()
        end = time.time()
        print('多进程',end-start) 
    

    多进程 5.499674558639526

    '''采用多线程计时情况'''
    
    def work1():
        x = 1+1
        time.sleep(5)
    
    
    if __name__ == '__main__':
        t_list = []
        start = time.time()
        for i in range(4):
            t = Thread(target=work1)
            # t = Process(target=work1)
            t_list.append(t)
            t.start()
        for t in t_list:
            t.join()
        end = time.time()
        print('多线程',end-start) 
    

    多线程 5.004202604293823

    小结:你发现了嘛!!!多线程的时间更短,相差0.5秒意味着什么!!!你明白吗???奶茶都可以绕地球两百圈了!!

    分析:多线程为什么更快?

    • 因为你看,多进程那么多个人同时去做,意味着卡机的时候都得哭着等。
    • 那线程就不一样了,我们可聪明了,谁要等你,我直接切切切,所以同一段当然更快咯,略略略~

    计算密集型

    '''采用多进程计时情况'''
    
    def work1():
        res=0
        for i in range(100000000): #1+8个0
            res*=i
    
    if __name__ == '__main__':
        t_list = []
        start = time.time()
        for i in range(4):
            t = Process(target=work1)
            t_list.append(t)
            t.start()
        for t in t_list:
            t.join()
        end = time.time()
        print('多进程',end-start)  
    

    多进程 18.062480211257935

    '''采用多线程计时情况'''
    
    def work1():
        res=0
        for i in range(100000000): 
            res*=i
    
    if __name__ == '__main__':
        t_list = []
        start = time.time()
        for i in range(4):
            t = Thread(target=work1)
            # t = Process(target=work1)
            t_list.append(t)
            t.start()
        for t in t_list:
            t.join()
        end = time.time()
        print('多线程',end-start)  
    

    多线程 33.27059483528137

    小结:这次就算周杰伦说好不哭也救不了,15秒可以说是天壤之别了。。。

    分析:多进程为什么更快?

    • 因为你看,这次不卡机了,所以多进程那么多个人同时去做一件事,意味着一个时间里只需要完成一件事就好啦!(一个任务时间)
    • 那多线程就不一样了,计算工作量大又耗时,但这是必经之路,这跟卡机不一样,因为那只有一个数据在动,而计算整个过程牵一发而动全身,所以一个时刻一条线程不断地切换,耍小聪明既会丢数据又没用!(多个任务时间)

    总结

    • IO密集型
      • 各个线程都会都各种的等待,多线程比较适合
      • 也可以采用多进程+协程
    • 计算密集型
      • 线程在计算时没有等待,这时候去切换,就是无用的切换,python不太适合开发这类功能
      • 推荐使用多进程
  • 相关阅读:
    Python语法解析器PLY——lex and yacc in Python
    spider-lang :爬虫语言,专为网络爬虫设计
    使用ANTLR做一个简单的Python SQL语法解析器
    使用Antlr实现简单的DSL
    Wrights Notes
    20个人团建能干些什么?
    zz
    贾跃亭反思乐视节奏过快_公司频道_财新网
    西湖人才网 职称考评
    安能物流
  • 原文地址:https://www.cnblogs.com/fxyadela/p/11545243.html
Copyright © 2020-2023  润新知