zoukankan      html  css  js  c++  java
  • 守护线程-线程锁-死锁现象

    一、守护线程

    主线程会等待子线程结束之后才结束,因为主线程结束进程就会结束,进程结束就会回收资源,而线程是进程的资源。

    守护线程随着主线程的结束而结束

    守护线程会在主线程的代码结束之后继续守护其他子线程,因为其他子线程未结束,主线程就未结束主进程也意味着未结束,那么守护线程就还没结束。

     

    守护进程:会随着主进程的代码结束而结束

    如果主进程结束之后还有其他子进程在运行,守护进程不守护。

    守护线程:随着主线程的结束而结束。

    如果主线程代码结束之后,还有其他子线程在运行,守护线程也守护

    因为:

    守护进程和守护线程的结束原理不同。

    守护进程需要主进程来回收资源

    守护线程是随着进程的结束才结束的

    其他子线程结束——>主线程结束——>主进程结束——>整个进程中所有的资源被回收——>守护线程同时被结束回收

     

    进程是资源分配单位

    子进程都需要它的父进程来回收资源

    线程是进程中的资源

    所有的线程都会随着进程的结束而被回收的

    守护线程例子1:

    from threading import Thread
    import time
    ​
    ​
    def func1():
        while True:
            print('守护线程func1')
            time.sleep(1)
    ​
    ​
    t1 = Thread(target=func1)
    t1.daemon = True    # 开启守护线程t1,daemon必须在start之前设置
    t1.start()
    time.sleep(3)       # 主线程结束,主进程结束,守护线程结束被回收资源
    # 输出
    守护线程func1
    守护线程func1
    守护线程func1

    守护线程例子2:

    from threading import Thread
    import time
    ​
    ​
    def func1():
        while True:
            print('守护线程func1')
            time.sleep(1)
    ​
    ​
    def func2():
        for _ in range(5):
            print('线程func2')
            time.sleep(1)
    ​
    ​
    t1 = Thread(target=func1)
    t1.daemon = True    # 开启守护线程t1,daemon必须在start之前设置
    t2 = Thread(target=func2)
    t1.start()
    t2.start()          # 守护线程会等待t2线程结束后才结束
    # 输出    (顺序不一定是这样的,但是都会打印5次)
    守护线程func1
    线程func2
    守护线程func1
    线程func2
    守护线程func1
    线程func2
    守护线程func1
    线程func2
    守护线程func1
    线程func2

    二、线程之间数据不安全现象

    因为线程之间数据是共享的,在多线程的情况下:

    如果在计算某一个全局变量的时候,还要进行赋值操作,这个过程不是由一条完整的CPU指令完成的。

    如果在判断某个bool表达式的之后,在做某些操作,这个过程也不是由一条完整的CPU指令完成的。

    在中间发生了GIL锁的切换,可能会导致数据不安全。

    但是对列表或者字典中的方法去操作全局变量的时候数据是安全的。

    列一: +=、-=、*=、/=、while、if都是数据不安全的,+ 和 赋值是分开的两个操作中间可能会被GIL锁切换

    from threading import Thread
    ​
    ​
    # 方法一:对count循环加1,五十万次
    def func1():
        for _ in range(500000):
            global count
            count += 1
    ​
    ​
    # 方法二:对count循环减1,五十万次
    def func2():
        for _ in range(500000):
            global count
            count -= 1
    ​
    ​
    count = 0
    t_l = []
    ​
    # 都开5个线程同样的加减,按理最后count的结果是0
    for _ in range(5):
        t1 = Thread(target=func1)
        t2 = Thread(target=func2)
        t1.start()
        t2.start()
        t_l.append(t1)
        t_l.append(t2)
    ​
    # 等待所有线程结束
    for i in t_l:
        i.join()
    print(count)  # ————>但是最后的结果每次都不一样!
    ​
    ​
    # 输出(结果每次运行都不一样)
    -614392

    例二:append、pop . . . . 等列表中的方法或者字典中的方法去操作全局变量的时候,数据是安全的。

    import time
    from threading import Thread
    ​
    ​
    # 方法一:对count列表循环增加1,五十万次
    def func1():
        for _ in range(500000):
            global count
            # 这里就不需要加锁了
            count.append(1)
    ​
    ​
    # 方法二:对count列表循环移除一个值,五十万次
    def func2():
        for _ in range(500000):
            global count
            if not count:
                """
                此处if也会有数据不安全
                当列表只有一个值时
                两个线程同时判断列表是否有值时,
                线程1得出的结果是有,这时GIL切换到另一个线程2
                线程2的判断还是有值,就会删除列表中的最后一个值,
                然后又会被GIL切换回线程1
                线程1回去接着删除的时候就已经没有了,会报错
                这种情况会根据个人电脑的性能决定
                最好在if这里加一把锁
                """
                time.sleep(0.00001)
            # 但是对列表的操作方法是由一条CPU指令完成的,并不会被GIL切换,就算是切换也只有
            # 增加或没增加,删除或没删除,切换回来还是继续,并不影响。
            count.pop()
    ​
    ​
    count = []
    t_l = []
    ​
    # 都开5个线程同样的加减,按理最后count的结果是空列表[]
    for _ in range(5):
        t1 = Thread(target=func1)
        t2 = Thread(target=func2)
        t1.start()
        t2.start()
        t_l.append(t1)
        t_l.append(t2)
    ​
    # 等待所有线程结束
    for i in t_l:
        i.join()
    print(count)
    ​
    # 输出
    []

    三、线程锁

    在多线程之间线程的数据也是不安全的,像这种先去做某些操作,在去对这个全局变量进行赋值的,这样所有的操作都是不安全的,或者先进行一个什么判断,在去做某些操作的,这样的情况都不安全,所以我们就在这种情况下加锁就行了

    为了避免写线程锁:一句话,线程不要操作全局变量,不要在类里操作静态变量

    +=、-=、*=、/=、while、if都是数据不安全的

    queue logging 数据安全的(内置了锁)

    线程加锁和进程加锁一样:

    from threading import Thread, Lock
    ​
    ​
    # 方法一:对count循环加1,五十万次
    def func1(lock):
        for _ in range(500000):
            global count
            with lock:  # 对count += 1 加锁
                count += 1
    ​
    ​
    # 方法二:对count循环减1,五十万次
    def func2(lock):
        for _ in range(500000):
            global count
            with lock:  # 对count -= 1 加锁
                count -= 1
    ​
    ​
    count = 0
    t_l = []
    lock = Lock()   # 生成一把锁
    # 都开5个线程同样的加减,按理最后count的结果是0
    for _ in range(5):
        t1 = Thread(target=func1, args=(lock,))
        t2 = Thread(target=func2, args=(lock,))
        t1.start()
        t2.start()
        t_l.append(t1)
        t_l.append(t2)
    ​
    # 等待所有线程结束
    for i in t_l:
        i.join()
    print(count)
    ​
    # 输出
    0

    拓展实例:单例模式

    未加锁的单例模式(基本每个线程开的空间不是同一个空间)

    from threading import Thread
    import time
    ​
    ​
    class A:
        __instance = None
        def __new__(cls, *args, **kwargs):
            if not cls.__instance:
                time.sleep(0.001)   # 模拟开空间延迟
                cls.__instance = super().__new__(cls)
            return cls.__instance
    ​
    ​
    def func():
        print(A())
    ​
    # 开5个线程
    for i in range(5):
        t = Thread(target=func)
        t.start()
        
    # 输出
    <__main__.A object at 0x00C4F640>
    <__main__.A object at 0x01969FE8>
    <__main__.A object at 0x01969C28>
    <__main__.A object at 0x01969D60>
    <__main__.A object at 0x01969E98>

    加锁后的单例模式:(每个线程开的空间都是同一个)

    from threading import Thread
    import time
    ​
    ​
    class A:
        __instance = None
        from threading import Lock
        lock = Lock()
    ​
        def __new__(cls, *args, **kwargs):
            with cls.lock:      # 对if判断这里加锁
                if not cls.__instance:
                    time.sleep(0.001)  # 模拟开空间延迟
                    cls.__instance = super().__new__(cls)
                return cls.__instance
    ​
    ​
    def func():
        print(A())
    ​
    # 开5个线程
    for i in range(5):
        t = Thread(target=func)
        t.start()
        
    # 输出
    <__main__.A object at 0x0121F4F0>
    <__main__.A object at 0x0121F4F0>
    <__main__.A object at 0x0121F4F0>
    <__main__.A object at 0x0121F4F0>
    <__main__.A object at 0x0121F4F0>

    四、互斥锁和递归锁

    Lock——>互斥锁:效率高

    RLock——>递归锁:效率相对较低

     

    互斥锁:不能在同一个线程中连续acquire多次,一次acquire必须对应一次release

    from Threading import Lock # 互斥锁 不能再同一个线程程中连续acquire多次
    lock = Lock()       # 创建锁
    lock.acquire()      # 拿钥匙
    print(1)
    lock.acquire()      # 拿钥匙——>会卡在这里,因为没有还钥匙,就拿不到钥匙,拿钥匙和还钥匙要成对出现
    print(2)
    lock.release()      # 还钥匙
    # 输出
    1

    递归锁:可以在同一个线程中连续acquire多次(一把钥匙开多个锁),但是有多少个acquire,就要有多少个release。

    from threading import Thread,RLock
    
    
    def func(rlock,i):
        rlock.acquire()
        rlock.acquire()
        print(f'{i},开始')
        rlock.release()
        rlock.release()
        print(f'{i},结束')
    
    
    rlock = RLock()
    for i in range(3):
        t1 = Thread(target=func, args=(rlock, i))
        t1.start()
        
    # 输出
    0,开始
    0,结束
    1,开始
    1,结束
    2,开始
    2,结束

    五、死锁现象

    死锁现象是怎么产生的?

    多把(互斥/递归)锁,并且在多个线程中,交叉使用(两把锁,在第一把锁没有释放之前另一个线程就获取第二把锁,条件是一个线程两把锁共同拥有才能执行)

    递归锁——>效率低——>但是解决死锁现象有奇效

    互斥锁——>效率高——>但是多把锁容易出现死锁现象

     

    怎么解决?

    如果是互斥锁出现了死锁现象,最快的解决方案就是把所有的互斥锁都改成一把递归锁,但是程序的效率会降低的。

    递归锁比较适用于临时解决一些死锁现象,互斥锁是比较高效的,能够处理多个线程之间数据安全的 ,但是多把互斥锁的交替使用就容易产生死锁现象

    例子一死锁现象:四个人吃面,一碗面,一个叉子,面有一把锁叉子有一把锁,吃面的条件是拿到面和叉子,或者拿到叉子和面,然后吃面!

    from threading import Thread, RLock
    import time
    
    
    def eat_noodle1(name, noodle_lock, fork_lock):
        noodle_lock.acquire()
        print(f'{name}抢到面了')
        fork_lock.acquire()
        print(f'{name}抢到叉子了')
        print(f'{name}在吃面')
        time.sleep(0.01)
        fork_lock.release()
        print(f'{name}放下叉子了')
        noodle_lock.release()
        print(f'{name}放下面了')
    
    
    def eat_noodle2(name, noodle_lock, fork_lock):
        fork_lock.acquire()
        print(f'{name}抢到叉子了')
        noodle_lock.acquire()
        print(f'{name}抢到面了')
        print(f'{name}在吃面')
        time.sleep(0.1)
        noodle_lock.release()
        print(f'{name}放下面了')
        fork_lock.release()
        print(f'{name}放下叉子了')
    
        
    n_lock = RLock()         # 面锁
    f_lock = RLock()         # 叉子锁
    
    Thread(target=eat_noodle1, args=('小杨', n_lock, f_lock)).start()
    Thread(target=eat_noodle2, args=('鲍勃', n_lock, f_lock)).start()
    Thread(target=eat_noodle1, args=('小红', n_lock, f_lock)).start()
    Thread(target=eat_noodle2, args=('艾伦', n_lock, f_lock)).start()
    
    # 输出
    小杨抢到面了
    小杨抢到叉子了
    小杨在吃面
    小杨放下叉子了
    鲍勃抢到叉子了        ——>小杨刚放下叉子,鲍勃就拿走了
    小杨放下面了
    小红抢到面了        ——>小杨刚放下面,小红就拿走了
    """
    鲍勃拿走了叉子,小红拿走了面,两个人包括其他人都不能吃面了,这种现象就叫锁死现象
    """

    递归锁快速解决死锁现象:把所有的锁都变成一把锁

    from threading import Thread, RLock
    import time
    
    
    def eat_noodle1(name, fork_lock,noodle_lock):
        noodle_lock.acquire()
        print(f'{name}抢到面了')
        fork_lock.acquire()
        print(f'{name}抢到叉子了')
        print(f'{name}在吃面')
        time.sleep(0.01)
        fork_lock.release()
        print(f'{name}放下叉子了')
        noodle_lock.release()
        print(f'{name}放下面了')
    
    
    def eat_noodle2(name, noodle_lock, fork_lock):
        fork_lock.acquire()
        print(f'{name}抢到叉子了')
        noodle_lock.acquire()
        print(f'{name}抢到面了')
        print(f'{name}在吃面')
        time.sleep(0.1)
        noodle_lock.release()
        print(f'{name}放下面了')
        fork_lock.release()
        print(f'{name}放下叉子了')
    
    
    rlock = n_lock = f_lock = RLock()    # 把所有的锁都变成一把锁
    
    
    Thread(target=eat_noodle1, args=('小杨', rlock, rlock)).start()
    Thread(target=eat_noodle2, args=('鲍勃', rlock, rlock)).start()
    Thread(target=eat_noodle1, args=('小红', rlock, rlock)).start()
    Thread(target=eat_noodle2, args=('艾伦', rlock, rlock)).start()
    
    # 输出    (每个人都吃到了面)
    小杨抢到面了
    小杨抢到叉子了
    小杨在吃面
    小杨放下叉子了
    小杨放下面了
    鲍勃抢到叉子了
    鲍勃抢到面了
    鲍勃在吃面
    鲍勃放下面了
    鲍勃放下叉子了
    小红抢到面了
    小红抢到叉子了
    小红在吃面
    小红放下叉子了
    小红放下面了
    艾伦抢到叉子了
    艾伦抢到面了
    艾伦在吃面
    艾伦放下面了
    艾伦放下叉子了

    五、队列

    线程之间数据安全的容器队列

    遵循先进先出原则

    import queue
    from queue import Empty
    
    q = queue.Queue()
    
    q.put(1)
    q.put(2)
    q.put(3)
    print(q.get())
    print(q.get())
    print(q.get())
    try:
        print(q.get_nowait())
    except Empty:
        pass
    print('列队为空继续其他内容!')
    
    # 输出
    1
    2
    3
    列队为空继续其他内容!

    六、栈

    遵循后进先出原则

    from queue import LifoQueue,Empty
    ​
    q = LifoQueue()
    ​
    q.put(1)
    q.put(2)
    q.put(3)
    print(q.get())
    print(q.get())
    print(q.get())
    try:
        print(q.get_nowait())
    except Empty:
        pass
    print('栈为空继续其他内容!')
    ​
    # 输出
    3
    2
    1
    栈为空继续其他内容!

    七、优先级队列

    最先输出列队里ASCLL码最小的

    from queue import PriorityQueue, Empty
    ​
    q = PriorityQueue()
    ​
    q.put(2)
    q.put(1)
    q.put(3)
    print(q.get())
    print(q.get())
    print(q.get())
    try:
        print(q.get_nowait())
    except Empty:
        pass
    print('栈为空继续其他内容!')
    ​
    # 输出
    1
    2
    3
    栈为空继续其他内容!
    学习之旅
  • 相关阅读:
    <庆余年>
    JUC-12.3-线程的调度
    JUC-12.1-线程池介绍
    JUC-12.2-线程池使用
    JUC-11-线程八锁
    JUC-10-ReadWriteLock读写锁
    JUC-9-线程按序交替
    JUC-8-lock和Condition使用
    JUC-7-lock接口
    xcode单词及回调
  • 原文地址:https://www.cnblogs.com/XiaoYang-sir/p/14787812.html
Copyright © 2011-2022 走看看