【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不太适合开发这类功能
    • 推荐使用多进程
posted @ 2019-09-18 20:45  fxyadela  阅读(208)  评论(0编辑  收藏  举报