守护线程-线程锁-死锁现象

一、守护线程

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

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

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

 

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

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

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

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

因为:

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

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

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

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

 

进程是资源分配单位

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

线程是进程中的资源

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

守护线程例子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
栈为空继续其他内容!
学习之旅
原文地址:https://www.cnblogs.com/XiaoYang-sir/p/14787812.html