python中的进程和线程
一、概念
1.什么是进程
计算机程序只不过是磁盘中可执行的二进制(或其他类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。
进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间、内存、数据栈及其它记录其运行轨迹的辅助数据。
操作系统管理在其上运行的所有进程,并为这些进程公平的分配时间,进程也可以通过fork和spawn操作来完成其它的任务。
不过各个进程有自己的内存空间、数据栈等,所以只能使用进程间通讯,而不能直接共享信息。
2.什么是线程
线程(有时候被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。
线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。
线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其他的线程运行,这叫做让步。
一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。
线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。
实际上,在单CPU的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其他的线程去运行。
每个线程都只做自己的事,在需要的时候跟其它的线程共享运行的结果。
当然,这样的共享并不是完全没有危险的。如果多个线程共享访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果的不一致问题。这叫做竞态条件。
幸运的是,大多数线程库都带有一系列的同步原语,来控制线程的执行和数据的访问。
3.进程与线程的关系
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其它线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
进程和线程的主要区别在于它们是不同的操作系统资源管理方式。
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。
线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,
所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大。
但对于一些要求同时进行并且又要共享某些变量的并发操作,只能使用线程,不能用进程。
(1)简而言之,一个程序至少有一个进程,一个进程至少有一个线程。
(2)线程的划分尺度小于进程,使得多线程程序的并发性高。
(3)另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,而极大的提高了程序的运行效率。
(4)线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。
但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
(5)从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。
但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的主要区别。
线程和线程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正好相反。线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
4.并行与并发
并行:两个或多个事件在同一时刻发生;
并发:两个或多个事件在同一时间间隔内发生。
在操作系统中个,并发是指一个时间段中有几个程序都处于已启动到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
在网络服务器上,并发是指同一时刻能处理的连接数。比如服务器建立了100个TCP连接,即服务器同时维护了100个socket,那么并发量就是1000。
并发的关键是有处理多个任务的能力,不一定要同时。
并行的关键是有同时处理多个任务的能力。
5.同步和异步
同步:发送方发送数据后,等待接收方发回响应以后才发下一个数据包的通讯方式。步骤一致。
异步:发送方发送数据后,不等待接收方发回响应,接着发送下一个数据包的通讯方式。
同步是阻塞模式,异步是非阻塞模式。
二、锁
1.lock
进程同步锁
我们知道进程一般都是并发执行,有时候并发执行特别混乱,必须让进程串行,虽然可以使用join,但是join不太灵活,这里就会使用到同步锁。
#进程同步锁 from multiprocessing import Process,Lock import os,time,random def work(lock): #上同一把锁 lock.acquire() print('%s is running'% os.getpid()) time.sleep(random.randint(1,3)) print("%s is done"% os.getpid()) lock.release() if __name__ == "__main__": lock = Lock() #和join一样,一把锁 #建一把锁,将其放在函数之中,每个进程都会调用 p1 = Process(target=work,args=(lock,)) p2 = Process(target=work,args=(lock,)) p3 = Process(target=work,args=(lock,)) p1.start() p2.start() p3.start() #执行结果: 4292 is running #一个进程执行完毕之后再执行下一个 4292 is done 11748 is running 11748 is done 6184 is running 6184 is done
线程互斥锁
我们知道线程是共享一个进程的地址空间的,改变地址空间中的环境变量就会改变所有线程的环境变量,但是如果我们想用线程来更改环境变量,必须使用互斥锁。
#线程互斥锁 from threading import Thread,Lock import time n = 100 def task(): global n with lock: temp = n time.sleep(0.1) n = temp-1 if __name__ == '__main__': lock = Lock() lst = [] for i in range(50): t = Thread(target=task) lst.append(t) #创建一个线程 t.start() #启动一个线程 for t in lst: t.join() print(n)
这里可以用一个例子来说明Lock和join的关系
如果使用join,那么就只有几个人能买到票,前面的没买到,后面的查票都不行,这个添加到查询上面的的锁不合适
from multiprocessing import Process import json,random,time,os def search(): #查票 with open("db.txt",encoding='utf-8') as f: dic=json.load(f) print("%s 剩余票数 %s" %(os.getpid(),dic['count'])) def get(): #抢票 with open("db.txt",encoding='utf-8') as read_f: dic = json.load(read_f) if dic['count'] > 0: dic['count'] -=1 time.sleep(random.randint(1,3)) #模拟手速+网速 with open("db.txt",'w', encoding='utf-8') as write_f: json.dump(dic,write_f) print("\033[43;1m%s抢票成功\033[0m "%os.getpid()) def task(): search() get() if __name__ == "__main__": for i in range(10): p = Process(target=task) p.start() p.join() #使用join永远只有第一个人能够抢到 #执行结果: 2408 剩余票数 2 2408抢票成功 11328 剩余票数 1 11328抢票成功 12364 剩余票数 0 16460 剩余票数 0 11848 剩余票数 0 11820 剩余票数 0 14792 剩余票数 0 14084 剩余票数 0 5276 剩余票数 0 6160 剩余票数 0
使用Lock就会相对好一些
from multiprocessing import Process,Lock import json,random,time,os def search(): #查票 with open("db.txt",encoding='utf-8') as f: dic=json.load(f) print("%s 剩余票数 %s" %(os.getpid(),dic['count'])) def get(): #抢票 with open("db.txt",encoding='utf-8') as read_f: dic = json.load(read_f) if dic['count'] > 0: dic['count'] -=1 time.sleep(random.randint(1,3)) #模拟手速+网速 with open("db.txt",'w', encoding='utf-8') as write_f: json.dump(dic,write_f) print("\033[43;1m%s抢票成功\033[0m "%os.getpid()) def task(lock): search() #查找可能是并发执行, lock.acquire() #抢票才要锁,这也是用join的另一个不同 get() lock.release() if __name__ == "__main__": lock = Lock() for i in range(10): p = Process(target=task,args=(lock,)) p.start() #锁和join都是把并发变为串行,但是锁比join灵活,lock能让局部串行,而lock只能让全局串行 #执行结果 13232 剩余票数 3 14856 剩余票数 3 12968 剩余票数 3 15992 剩余票数 3 7880 剩余票数 3 10200 剩余票数 3 13100 剩余票数 3 7772 剩余票数 3 14252 剩余票数 3 352 剩余票数 3 2680 剩余票数 3 2116 剩余票数 3 17380 剩余票数 3 4476 剩余票数 3 12396 剩余票数 3 15932 剩余票数 3 13232抢票成功 12368 剩余票数 2 10916 剩余票数 2 9156 剩余票数 2 9864 剩余票数 2 14856抢票成功 12968抢票成功
可以看出Lock和join虽然在功能上类似,但是复杂情况,Lock机制还是更灵活
2.GIL
python代码的执行由Python虚拟机(也叫解释器主循环)来控制。
python在设计之初就考虑要在主循环中,同时只有一个线程执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU执行。
同样的,虽然Python解释器中可以“运行”多个线程,但在任意时刻,只有一个线程在解释器中运行。
对python虚拟机的访问由全局解释器锁来控制,正是这个锁能保证同一时刻只有一个线程在执行。
在多线程环境中,python虚拟机按一下方式执行。
(1)设置GIL
(2)切换到一个线程去运行
(3)运行:a.指定数量的字节码的指令,或者; b.线程主动让出控制
(4)把线程设置为睡眠状态
(5)解锁GIL
(6)再次重复以上所有步骤
在调用外部代码的时候,GIL将会被锁定,直到这个函数结束为止。
当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用Python退出进程的标准方法。
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。
from multiprocessing import Process from threading import Thread import os,time def work(): res=0 for i in range(100000000): res*=i if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(4): p=Process(target=work) #耗时5s多 p=Thread(target=work) #耗时18s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
from multiprocessing import Process from threading import Thread import threading import os,time def work(): time.sleep(2) print('===>') if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(400): # p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上 p=Thread(target=work) #耗时2s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
三、线程和进程的开启方式
开启进程有两种方式:
#第一种方式: from multiprocessing import Process import time,random def task(name): print('%s is running'%name) time.sleep(random.randint(1,5)) print('%s is ku'%name) if __name__ == "__main__": p1 = Process(target=task,args=('kebi',)) p1.start() #执行我i先后 #第二种方式(很少使用) from multiprocessing import Process import time class Myclass(Process): def __init__(self,name): super().__init__() self.name = name def run(self): time.sleep(2) print('%s is running '% self.name) if __name__ == '__main__': p = Myclass('cx') p.start() #开启进程的第二种方式,之前给Procee传参,现在是使用继承
同样的,开启线程也有两种方式
#第一种方式 from threading import Thread import time,os def task(): print('%s is running%s'% (os.getpid(),os.getppid())) time.sleep(2) print('%s is done'%os.getpid()) if __name__ =="__main__": t = Thread(target=task) t.start() print("主",os.getpid()) #第二种方式(使用的少) from threading import Thread import time,os class Mythread(Thread): def __init__(self,name): super().__init__() self.name = name def run(self): print('%s is running' % os.getpid()) time.sleep(2) print('%s is done' % os.getpid()) if __name__ == "__main__": t = Mythread('kebi') t.start()
四、进程和线程的名称空间
进程的名称空间:进程有独立的地址空间,所以各个进程之间不会相互影响。
线程的名称空间:同一个进程中的线程共享一个名称空间。
#进程 from multiprocessing import Process def task(): global n n=0 n = 100 if __name__ =="__main__": p = Process(target=task) p.start() print(n) #执行结果 100
#线程 from threading import Thread def task(): global n n=0 n = 100 if __name__ =="__main__": t = Thread(target=task) t.start() print(n) #执行结果 0
五、进程池和线程池
进程池内部维护一个进程序列,当使用时,则去进程池中的获取一个进程,如果进程池序列中没有可供使用的进程,那么进程就会等待,直到进程池中有可用进程为止,线程池也是类似。
进程池:
from concurrent.futures import ProcessPoolExecutor import os,random def func(name): print("%s吃了又一碗饭:%s" %(name,os.getpid())) time.sleep(random.randint(1, 3)) if __name__ == "__main__": p = ProcessPoolExecutor(3) #创建一个进程池,里面容纳3个进程 for i in range(7): obj = p.submit(func,'科比%i'%i) p.shutdown(wait=True) #类似与join,并且可以关门,以防在等的过程中又提交新的任务 print("主进程") #执行结果 科比0吃了又一碗饭:13980 科比1吃了又一碗饭:9636 科比2吃了又一碗饭:12660 科比3吃了又一碗饭:13980 科比4吃了又一碗饭:12660 科比5吃了又一碗饭:9636 科比6吃了又一碗饭:13980 主进程
线程池:
from threading import Thread,current_thread from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor import time,os def task(n): print('%s is running'% current_thread().getName()) time.sleep(3) return n**2 if __name__ =="__main__": t = ThreadPoolExecutor(3) #默认是CPU的核数*5 objs = [] for i in range(7): obj = t.submit(task,i) objs.append(obj) t.shutdown(wait=True) #异步,最终打印结果 for obj in objs: print(obj.result()) print("主",current_thread().getName()) #执行结果 ThreadPoolExecutor-0_0 is running ThreadPoolExecutor-0_1 is running ThreadPoolExecutor-0_2 is running ThreadPoolExecutor-0_2 is running ThreadPoolExecutor-0_1 is running ThreadPoolExecutor-0_0 is running 0 1 4 ThreadPoolExecutor-0_1 is running 9 16 25 36 主 MainThread
六、守护进程和守护线程
无论是进程还是线程,当主进程结束的时候,守护进程也会随之结束。
对于进程来说,所谓的主进程结束就是不包括子进程;对于线程来说,所谓的主进程结束是包括其中线程都结束。
守护进程:守护进程必须在子线程开启之前设置,而且守护进程不能有子进程。
#守护进程 from multiprocessing import Process import os,time,random def task(): print('%s is running '%os.getpid()) time.sleep(2) print('%s is done' % os.getppid()) if __name__ == "__main__": p = Process(target=task) p.daemon = True #1.必须在进程开启之前 2.不能再开启子进程 p.start() print("主") #执行结果 主 #子进程还没开始,主进程就已经结束了 变一变条件也许会更加明了 #让主进程再多执行几秒,守护进程就可以执行部分代码 from multiprocessing import Process import os,time,random def task(): print('%s is running '%os.getpid()) time.sleep(2) print('%s is done' % os.getppid()) if __name__ == "__main__": p = Process(target=task) p.daemon = True #1.必须在进程开启之前 2.不能再开启子进程 p.start() time.sleep(2) print("主") #执行结果 3760 is running #有新的东西了 主
守护线程:设置守护线程必须在子线程开始之前,必须是所有子线程都执行完,,主进程才会结束
from threading import Thread import os,time,random def task(): print('%s is running '%os.getpid()) time.sleep(2) print('%s is done' % os.getppid()) if __name__ == "__main__": p = Thread(target=task) p.daemon = True #主线程结束,杀子线程 p.start() #守护线程里面可以再开子线程 print("主") #所有的非守护线程都死了守护进程才会死,次出之所以能打印,主线程执行需要时间 #执行结果 14980 is running #守护线程并没有执行完毕 主 现在我添加一个子线程 from threading import Thread import os,time,random def task(): print('%s is running '%os.getpid()) time.sleep(2) print('%s is done' % os.getppid()) def mask(): print("%s start...."% os.getpid()) time.sleep(3) print("%s over...."% os.getpid()) if __name__ == "__main__": p = Thread(target=task) p1 = Thread(target=mask) p.daemon = True #主线程结束,杀子线程 p.start() #守护线程里面可以再开子线程 p1.start() print("主") #所有的非守护线程都死了守护进程才会死,次出之所以能打印,主线程执行需要时间 #执行结果 15896 is running 15896 start.... 主 14716 is done 15896 over.... #由于子进程的执行时间非常长,因此守护线程也能全部执行完 与守护进程不同的是,守护线程可以有子进程 from threading import Thread import os,time,random def task(): c = Thread(target=mask) c.start() print('%s is running '%os.getpid()) time.sleep(2) print('%s is done' % os.getppid()) def mask(): print("守护线程中的子线程") if __name__ == "__main__": p = Thread(target=task) p.daemon = True p.start() #守护线程里面可以再开子线程 time.sleep(3) #需要主线程的执行时间稍微长一点 print("主")
七、进程队列和线程队列
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocess模块支持两种形式:队列和管道。
这两种方式都是使用消息传递的。队列是基于管道和锁来实现。
进程队列
from multiprocessing import Queue q = Queue() #默认依托内存大小,不要传太大的东西 q.put({"name":"kebi"})#可以放任意类型的数据 q.put((1,2,3)) q.put('everything is possible') print(q.get()) print(q.get()) print(q.get()) print(q.get()) #执行结果 {'name': 'kebi'} (1, 2, 3) everything is possible
线程队列
用法基本与进程队列一样
有几种模式常用:先进先出、后进先出、优先级。
import queue # q = queue.LifoQueue() q = queue.Queue() #先进先出 q.put("one")#可以放任意类型的数据 q.put("two") q.put("three") print(q.get()) print(q.get()) print(q.get()) #执行结果 one two three
#栈 import queue q = queue.LifoQueue() q.put("one")#可以放任意类型的数据 q.put("two") q.put("three") print(q.get()) print(q.get()) print(q.get()) #执行结果 three two one
#优先级 import queue q = queue.PriorityQueue() q.put((10,'one')) q.put((-1,'two')) q.put((5,'three')) print(q.get()) print(q.get()) print(q.get()) #执行结果 (-1, 'two') (5, 'three') (10, 'one')
八、信号量
信号量主要是用来维持有限的资源,使得在一定的时间内使用该资源的线程进程只是指定的数量
信号量与进程池是完全不同的概念,信号量是多个进程共用有限资源,而进程池多个任务用指定个进程来实现。
进程池
#三个人做10个工作 from concurrent.futures import ProcessPoolExecutor import os,time,random def job(name): print("%start.....%s"% (name,os.getpid())) time.sleep(1) # print("%sover.....%s"% (name,os.getpid())) names = ['kebi','maoxian','xiaoniao'] if __name__ == "__main__": p = ProcessPoolExecutor(3) for i in range(10): name = names[random.randint(0, 2)] obj=p.submit(job,name) obj.result() p.shutdown(wait=True) #执行结果 xiaoniaotart.....4156 maoxiantart.....5544 kebitart.....3084 xiaoniaotart.....4156 maoxiantart.....5544 kebitart.....3084 maoxiantart.....4156 xiaoniaotart.....5544 kebitart.....3084 kebitart.....4156
#10个人要上厕所,厕所只有三个 from multiprocessing import Process,Semaphore import time,random,os def task(num): print("%s 上厕所%d"%(os.getpid(),num)) time.sleep(random.randint(1,3)) if __name__ == "__main__": sm = Semaphore(3) for i in range(10): num = random.randint(1,3) p = Process(target=task,args=(num,)) p.start() #执行结果 8544 上厕所3 7868 上厕所2 5888 上厕所2 5736 上厕所2 10440 上厕所2 15564 上厕所1 16320 上厕所1 14360 上厕所2 1824 上厕所2 6528 上厕所3
九、生产者消费者模型
1.为什么要使用生产着消费者模型
在线程世界里,生产者就是生产数据的线程,消费者就是消费消费数据的线程。
在多进程开发当中,如果生产者处理很快,消费者处理很慢,那么生产者就必须等待消费者处理完,才能继续生产数据,
同样的道理,如果消费者的处理能力大于生产者。为了解决生产者和消费者强耦合造成的时间浪费问题,引入了生产者和消费者模型
2.什么是生产者消费者模型
生产者消费者模型是通过一个容器来解决生产者和消费者的强耦合问题,
生产者和消费者彼此之间不能直接通讯,而通过阻塞队列来通讯,所以生产者生产完数据之后不用等待消费者处理,直到仍给阻塞队列,
消费者不找生产者要数据,而是直接从阻塞队列取值,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
3.实例
import time,os,random from multiprocessing import Queue,Process def procducer(q): #定义一个生产者函数 for i in range(1,11): res = "第%s个包子" %i time.sleep(0.5) q.put(res) #往框里面放一个包子 print('%s 生产了 %s'%(os.getpid(),res)) def consumer(q): #定义一个消费者函数 while True: res = q.get() if res is None: break print('\033[43;1m%s 吃了%s\033[0m'%(os.getpid(),res)) time.sleep(random.randint(1,2)) if __name__ == '__main__': q = Queue() #队列 p = Process(target=procducer,args=(q,)) #进程 c = Process(target=consumer,args=(q,)) # p.start() c.start() p.join() #程序停不下来,主进程没死,消费者没死,通过判断是否为空来判断是否结束不靠谱 q.put(None) #什么时候算生产完,怎么确定生产完了,p.join肯定完了 #放一个None进去 #队列可以实现生产者和消费者之间的解耦 #执行结果 8920 生产了 第1个包子 18304 吃了第1个包子 8920 生产了 第2个包子 8920 生产了 第3个包子 8920 生产了 第4个包子 18304 吃了第2个包子 8920 生产了 第5个包子 8920 生产了 第6个包子 8920 生产了 第7个包子 8920 生产了 第8个包子 18304 吃了第3个包子 8920 生产了 第9个包子 8920 生产了 第10个包子 18304 吃了第4个包子 18304 吃了第5个包子 18304 吃了第6个包子 18304 吃了第7个包子 18304 吃了第8个包子 18304 吃了第9个包子 18304 吃了第10个包子
import time,os,random from multiprocessing import Queue,Process,JoinableQueue def procducer(food,q): #定义一个生产者函数 for i in range(1,4): res = "%s%s" %(food,i) time.sleep(0.5) q.put(res) #往框里面放一个包子 print('%s 生产了 %s'%(os.getpid(),res)) q.join() #不生产了,等东西被取完,等消费者发消息 def consumer(q): #定义一个消费者函数 while True: res = q.get() # if res is None: # break print('\033[43;1m%s 吃了%s\033[0m'%(os.getpid(),res)) time.sleep(random.randint(1,2)) q.task_done() #发信号的操作 if __name__ == '__main__': q = JoinableQueue() p1 = Process(target=procducer,args=('包子',q,)) p2 = Process(target=procducer,args=('玉米',q,)) p3 = Process(target=procducer,args=('狗粮',q,)) c1 = Process(target=consumer,args=(q,)) c2 = Process(target=consumer,args=(q,)) c1.daemon=True c2.daemon=True p1.start() p2.start() p3.start() c1.start() c2.start() p1.join() p2.join() p3.join() #生产者结束,——》q.join()——》消费者确实把所有数据都收到 #主进程结束之后,守护线程随之结束,这样消费者进程就不会存活
十、死锁现象与递归锁
进程也有死锁与递归锁,所谓死锁,是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些在互相等待的进程称为死锁进程,如下就是死锁
from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() ''' Thread-1 拿到A锁 Thread-1 拿到B锁 Thread-1 拿到B锁 Thread-2 拿到A锁 然后就卡住,死锁了 '''
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock