主线程会等待子线程结束之后才结束,因为主线程结束进程就会结束,进程结束就会回收资源,而线程是进程的资源。
守护线程随着主线程的结束而结束
守护线程会在主线程的代码结束之后继续守护其他子线程,因为其他子线程未结束,主线程就未结束主进程也意味着未结束,那么守护线程就还没结束。
守护进程:会随着主进程的代码结束而结束
如果主进程结束之后还有其他子进程在运行,守护进程不守护。
守护线程:随着主线程的结束而结束。
如果主线程代码结束之后,还有其他子线程在运行,守护线程也守护
因为:
守护进程和守护线程的结束原理不同。
守护进程需要主进程来回收资源
守护线程是随着进程的结束才结束的
其他子线程结束——>主线程结束——>主进程结束——>整个进程中所有的资源被回收——>守护线程同时被结束回收
进程是资源分配单位
子进程都需要它的父进程来回收资源
线程是进程中的资源
所有的线程都会随着进程的结束而被回收的
守护线程例子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 栈为空继续其他内容!
七、优先级队列
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 栈为空继续其他内容!