Python 线程

 主要内容:

一. 背景知识

  1. 进程

  2. 有了进程为何还要线程?

  3. 线程的出现

二. 进程和线程的关系

  进程与线程的区别

三. 线程的特点

  1. 轻型实体

  2. 独立调度和分派的基本单位

  3. 共享进程资源

  4. 可并发执行

四. Python与线程

五. Threading模块

  1. 线程创建的两种方式

  2. 进程与线程开启效率比较

  3. 同一进程下线程是资源共享的

  4. 线程共享数据时,数据是不安全的

  5. 守护线程

六. 信号量

七. 锁

  1. GIL锁

  2. 同步锁

  3. 死锁与递归锁

 

 

一. 背景知识

1. 进程

在已经了解了操作系统中进程的概念后,我们对进程有了一定的了解: 程序是不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程. 程序和进程的区别就在于: 程序是指令的集合, 它是进程运行的静态描述文本; 进程是程序的一次执行活动,属于动态概念. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行. 这样的设计,大大他搞了CPU的利用率. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的.

 

2. 有了进程为何还要线程?

(1)什么是线程?线程指的是流水线式的工作过程,一个进程内最少自带一个线程,其实进程根本不能执行,进程不是执行单位,而是资源单位,分配资源的单位.线程才是执行单位.

(2)进程与线程的对比:

  a.同一个进程内的多个线程是共享该进程的资源的,不同进程内的线程资源是隔离的.

  b.创建线程对资源的消耗远远小于创建进程的消耗

(3)进程有很多优点,它提供了多道编程,提高了计算机的利用率,让每个人感觉自己独享着CPU和其他资源.然而,进程也是有缺点的,主要体现在两点上:

  a.进程只能在同一时间执行一个任务,如果想要同时执行两个或多个任务,进程就无能为力了.

  b.进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行.

 

3. 线程的出现

60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建,撤销与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程运行开销过大.

因此,在80年代,出现了能独立运行的基本单位--线程(Threads).

需要注意的是: 进程是资源分配的最小单位,线程是CPU调度的最小单位.每个进程中至少有一个线程.

在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程.我们可以把一个车间的工作过程看作是一个进程,把一条流水线工作的过程看作是一个线程.车间不仅要负责把资源整合到一起,而且一个车间内至少要有一条流水线.

所以总结来说: 进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位.

多线程的概念是: 在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间.

 

 

二. 进程和线程的关系

线程与进程的区别可归纳为以下4点:

1. 地址空间和其他资源(如打开文件): 进程间相互独立,同一进程的各线程间共享. 某进程内的线程在其他进程不可见.

2. 通信: 进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信----需要进程同步和互斥手段的辅助,以保证数据的一致性(类似于进程中的锁的作用).

3. 调度和切换: 线程上下文切换比进程上下文切换要快得多.

4. 在多线程操作系统中,进程不是一个可执行的实体,真正去执行程序的不是进程,而是线程.可以理解为进程就是一个线程的容器.

 

 

三. 线程的特点

在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体. 线程具有以下属性:

1. 轻型实体

线程中的实体基本上不拥有系统资源,只是有一些必不可少的,能保证独立运行的资源.

线程的实体包括程序,数据和TCB.线程是动态概念,它的动态特性有线程控制块TCB(Thread Control Block)描述.

#TCB包括以下信息:
(1)线程状态.
(2)当线程不运行时,被保存的现场资源.
(3)一组执行堆栈.
(4)存放每个线程的局部变量主存区.
(5)访问同一个进程中的主存和其它资源.
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈.

2. 独立调度和分派的基本单位

在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位.由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的线程).

3. 共享进程资源

线程在同一进程中的各个线程, 都可以共享该进程所拥有的资源, 这首先表现在: 所有线程都具有相同的进程id, 这意味着, 线程可以访问该进程的每一个内存资源; 此外, 还可以访问进程所拥有的已打开文件、定时器、信号量机构等. 由于同一个进程内的线程共享内存和文件, 所以线程之间互相通信不必调用内核.

4. 可并发执行

在一个进程中的多个线程之间, 可以并发执行, 甚至允许在一个进程中所有线程都能并发执行; 同样, 不同进程中的线程也能并发执行, 充分利用和发挥了处理机与外围设备并行工作的能力.
 
 
 
四. Python与线程
 
1. 全局解释器锁GIL
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制. Pyhton在设计之初就考虑到: 在主循环中同时只有一个线程在执行. 虽然Python解释器中可以"运行"多个线程,但在任意时刻只有一个线程在解释器中运行. 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行.在多线程环境中,Python虚拟机按以下方式执行:
  a. 设置GIL
  b. 切换到一个线程去执行
  c. 运行指定数量的字节码指令或者线程主动让出控制(可以调用time.sleep(0))
  d. 把线程设置为睡眠状态
  e. 解锁GIL
  f. 再次重复以上所有步骤
在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL.
 
2. Python线程模块的选择
Pyhton提供了几个用于多线程编程的模块,包括thread,threading和Queue等. thread和threading模块允许程序员创建和管理线程. thread模块提供了基本的线程和锁的支持,threading提供了更高级别,功能更强的线程管理的功能. Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构.
避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突; 其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程序结束时,所有的线程都会被强制结束掉,没有警告也没有正常的清除工作,至少threading模块能确保重要的子线程退出后程序才退出.
就像我们熟悉的time模块,它比其他模块更接近底层,越是接近底层,用起来也越麻烦,就像时间日期转换之类的就比较麻烦,但是后面我们学到一个datetime模块,提供了更为简便的时间日期处理方法,它是建立在time模块的基础上来的. 又如socket和socketserver(底层还是用的socket)等等,这里的threading就是thread的高级模块.
thread模块不支持守护进程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出.而threading模块支持守护进程,守护进程一般是一个等待客户请求的服务器,如果没有客户提出请求它就会在那里等待,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出.
 
 
 
 
五. Threading模块
multiprocess模块完全模仿了threading模块的接口,二者在使用层面有很大的相似性.
1. 线程创建的两种方式
import time
from threading import Thread    # 引入线程模块

def func(n):    # 自定义一个函数
    time.sleep(1)
    print(n)

if __name__ == '__main__':
    t = Thread(target=func, args=("hello,world",))  # 创建线程对象
    t.start()   # 开启线程
    print("主线程结束")
创建线程方式一
import time
from threading import Thread    # 引入线程模块

class MyThread(Thread): # 自定义一个类
    def __init__(self, n):  # 传参n
        super().__init__()  # 自己想要传参,必须先super()执行父类的init方法,再写自己的实例变量
        self.n = n
    def run(self):  # 自定义一个run()方法,内容不定,但是run名称不能变
        time.sleep(1)
        print(self.n)

if __name__ == '__main__':
    t = MyThread("hello,world")  # 创建线程对象
    t.start()   # 开启线程
    print("主线程结束")
创建线程方式二

 

2. 进程与线程开启效率比较

import time
from threading import Thread
from multiprocessing import Process

def func(n):
    sum = 0
    for i in range(n):
        sum += i

if __name__ == '__main__':
    t_start_time = time.time()  #开始时间
    t_list = []
    for i in range(10):
        t = Thread(target=func, args=(100,))
        t.start()
        t_list.append(t)
    [tt.join() for tt in t_list]
    t_end_time = time.time()    #结束时间
    t_dif_time = t_end_time - t_start_time  #时间差

    p_start_time = time.time()  #开始时间
    p_list = []
    for ii in range(10):
        p = Process(target=func, args=(100,))
        p.start()
        p_list.append(p)
    [pp.join() for pp in p_list]
    p_end_time = time.time()    #结束时间
    p_dif_time = p_end_time - p_start_time  #时间差

    print("线程的执行时间是>>>", t_dif_time)
    print("进程的执行时间是>>>", p_dif_time)
    print("主线程结束")

# 执行结果:
# 线程的执行时间是>>> 0.0010013580322265625
# 进程的执行时间是>>> 0.37227368354797363
# 主线程结束
进程与线程开启效率比较

从上面代码的结果中可以看出: 执行同一个任务,线程的执行时间远远小于进程的执行时间.因此,线程的效率是比较高的.

 

3. 同一进程下线程是资源共享的

from threading import Thread

num = 100
def func():
    global num
    num = 0

if __name__ == '__main__':
    t = Thread(target=func,)
    t.start()
    t.join()
    print(num)

# 执行结果:
# 0
同一进程下线程是资源共享的

 

4. 线程共享数据时,数据是不安全的

import time
from threading import Thread

num = 100   #全局变量
def func():
    global num
    # 模拟num-=1的"取值->计算->赋值"过程
    mid = num       #取值
    mid = mid - 1   #计算
    time.sleep(0.0001)
    num = mid       #赋值

if __name__ == '__main__':
    t_list = []
    for i in range(10): #创建10个子线程
        t = Thread(target=func,)
        t.start()
        t_list.append(t)
    [tt.join() for tt in t_list]    # 主线程等待子线程执行结束
    print('主线程结束,此时全局变量为>>>', num)

# 执行结果:
# 主线程结束,此时全局变量为>>> 99
演示共享资源的时候,数据不安全的问题
import time
from threading import Thread, Lock

num = 100   # 全局变量
def func(t_lock):
    global num
    t_lock.acquire()    # 加锁
    # 模拟num-=1的"取值->计算->赋值"过程
    mid = num       # 取值
    mid = mid - 1   # 计算
    time.sleep(0.001)
    num = mid       # 赋值
    t_lock.release()    # 解锁

if __name__ == '__main__':
    t_lock = Lock() # 创建同步锁(互斥锁)对象
    t_list = []
    for i in range(10): # 创建10个子线程
        t = Thread(target=func, args=(t_lock,))
        t.start()
        t_list.append(t)
    [t.join() for t in t_list]  # 主线程等待子线程执行结束
    print('主线程结束,此时全局变量为>>>', num)
    
# 执行结果:
# 主线程结束,此时全局变量为>>> 90
通过引入线程模块里的锁来解决数据不安全的问题

 

5. 守护线程

无论是进程还是线程,都遵循: 守护进程(线程)会等待主进程(线程)运行完毕后被销毁. 需要强调的是: 运行完毕并非终止运行.

#1. 对于主进程来说,运行完毕指的是主进程代码运行完毕
#2. 对于主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程全部执行完毕,主线程才算运行完毕.

详细解释:

#1. 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束.
#2. 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收). 因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束,因为进程执行结束是要回收资源的.

观察以下两个例子,对比两个例子中函数的睡眠时和执行结果:

import time
from threading import Thread

def func1(n):
    time.sleep(4)
    print(n)

def func2(n):
    time.sleep(2)
    print(n)

if __name__ == '__main__':
    t1 = Thread(target=func1, args=("我是子线程1号",))
    t1.daemon = True    #设置守护线程
    t1.start()
    t2 = Thread(target=func2, args=("我是子线程2号",))
    t2.start()
    print("主线程结束")

# 执行结果:
# 主线程结束
# 我是子线程2号
例1:守护线程func1睡眠4秒,非守护线程func2睡眠2秒
import time
from threading import Thread

def func1(n):
    time.sleep(1)
    print(n)

def func2(n):
    time.sleep(2)
    print(n)

if __name__ == '__main__':
    t1 = Thread(target=func1, args=("我是子线程1号",))
    t1.deamon = True    # 设置守护线程,必须放到start前面
    t1.start()
    t2 = Thread(target=func2, args=("我是子线程2号",))
    t2.start()
    print("主线程结束")

# 执行结果:
# 主线程结束
# 我是子线程1号
# 我是子线程2号
例2:守护线程func1睡眠1秒,非守护线程func2睡眠2秒

对比两个例子的执行结果,可以看出,主线程等待所有非守护线程的结束才结束.当主线程的代码运行结束后,还要等待非守护线程执行完毕,在这个等待的过程中,守护线程并没有消亡,还在继续执行.

对比守护进程与守护线程:

  守护进程: 主进程的代码执行完毕后,整个程序并没有结束,且主进程仍然存在着,因为主进程要等待其它子进程执行完毕,回收子进程的残余资源(为子进程收尸).总之,主进程的代码执行完毕后守护进程也跟着结束----守护进程随着主进程的消亡而消亡.

  守护线程: 主线程的代码执行完毕后,整个程序并没有结束,且主线程仍然存在着,因为主线程要等待所有非守护线程执行完毕,随后,当所有线程全部执行完毕后,主线程结束,这也意味着主进程的结束,最后主进程回收所有资源.总之,主线程的代码执行完毕后要等待非守护线程执行完毕,在这个等待过程中,守护进程没有消亡,直到等待结束,随主线程的消亡而消亡.

 

 

六. 信号量

同进程的一样, Semaphore管理一个内置的计数器:
每当调用acquire()时内置计数器-1; 
调用release()时内置计数器+1;
计数器不能小于0;当计数器为0时, acquire()将阻塞线程直到其他线程调用release().
from threading import Thread,Semaphore
import threading
import time

def func():
    if sm.acquire():    # 加锁
        print(threading.currentThread().getName() + " get semaphore")
        time.sleep(3)
        sm.release()    # 解锁

if __name__ == '__main__':
    sm = Semaphore(5)   # 创建信号量对象,限制锁内每次只能进入5个线程
    for i in range(25): # 创建25个线程
        t = Thread(target=func,)
        t.start()
例1
from threading import Thread,Semaphore
import threading
import time

def func():
    sm.acquire()    # 加锁
    print('%s get semaphore' % threading.current_thread().getName())
    time.sleep(3)
    sm.release()    # 解锁

if __name__ == '__main__':
    sm = Semaphore(5)   # 创建信号量对象,限制锁内每次只能进入5个线程
    for i in range(25): # 创建25个线程
        t = Thread(target=func,)
        t.start()
例2
 
总结:
  信号量: 控制同时能够进入锁内去执行代码的线程数量维护了一个计数器刚开始创建信号量的时候假如设置的是5个房间(sm = Semaphore(5)), 一个房间每次只能进一个线程. 一个线程进入一次acquire那么sm就减1, 出来一次sm就+1, 如果计数器为0, 那么acquire()将阻塞住, 其他的线程就需要等待这样其他的线程和正在执行的这一组(5个)线程就是一个同步的状态而进入acquire里面去执行的那5个线程则是异步的.

 

 

七. 锁

1. GIL锁(Global Interpreter Lock)

 待续

 

2. 同步锁(互斥锁)

三个需要注意的点:
#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来

#2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高

 

# GIL VS Lock

既然Python已经有了一个GIL来保证同一时间只能有一个线程Laura执行,那么为什么还需要Lock呢?
首先我们需要达成共识: 锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据.
然后,我们可以得出结论: 保护不同的数据就应该加不同的锁.
于是,问题就比较明朗了,GIL与Lock是两把锁,保护的数据不一样:前者是解释器级别的(保护的是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock.

# 过程分析: 所有线程抢的是GIL锁,或者说所有线程抢的是执行权限.
线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕(即线程1还未释放Lock),此时有可能线程2抢到了GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,接着被夺走执行权限,这个时候有可能线程1拿到GIL,然后正常执行到释放Lock.以上过程就导致了串行运行的效果.

既然是串行,那么我们执行:
t1.start()
t1.join()
t1.start()
t1.join()
这同样也是串行,为何还要加Lock呢?我们要知道join等待的是所有的代码执行完毕,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码.

  GIL锁出现的原因:

因为Python解释器帮助我们自动定期进行内存回收,我们可以认为Python解释器里有一个独立的线程,每过一端时间它就wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时我们自己程序里的线程和py解释器的线程是并发运行的,假如此时我们的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其他线程正好又重新给这个还没来得及清空的内存空间赋值了,结果就有可能是新赋值的数据被删除了.为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程进入python解释器运行时,其它线程都不能进入了,这样就解决了上述的问题.可以说这是Python早期版本的遗留问题.

  举例说明:

from threading import Thread,Lock
import time

def work():
    global n
    # lock.acquire()  # 加锁
    temp = n        # 取值
    time.sleep(0.0001)  # 模拟延迟
    n = temp - 1    # 计算和赋值
    # lock.release()  # 解锁

if __name__ == '__main__':
    lock = Lock()   # 创建锁
    n = 100         # 全局变量
    t_list = []
    for i in range(100):
        t = Thread(target=work,)
        t_list.append(t)
        t.start()
    for p in t_list:
        p.join()
    print(n)
例1

在例1的代码中,如果在work()函数中不加锁,执行结果是93~96的其中一个数字,而加锁后的结果就是0.可以清晰的判断出,加锁后的结果才是我们想要的结果. 而前者出现的原因则是: 当我们同时开启100个线程时,由于线程开启的时间非常短,当第一个线程拿到全局变量n=100后,就阻塞在time.sleep()处,在这个阻塞的过程中,由于该线程只是拿到了n=100却还未来得及计算并重新赋值,于是下一个线程就拿到了相同的n=100,同样也阻塞在time.sleep()处,以此类推,导致最后的执行结果不符合我们的预期.

 

 

 

3. 死锁与递归锁

进程与线程都有死锁. 因为一般情况下进程之间是数据不共享的,所以不需要加锁. 而由于线程是对全局的数据共享的,所以对于全局的数据进程操作的时候,需要加锁.

死锁: 死锁是指当我们使用锁嵌套锁的时候,多个线程异步执行会出现线程之间因为争夺资源而造成的一种互相等待(对方未释放的锁)的现象, 若无外力作用, 它们都将无法推进下去. 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程.  如下实例:

from threading import Lock as Lock
import time
mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

复杂一些的死锁现象:

from threading import Thread, Lock
import time

mutexA = Lock() # 创建Lock对象
mutexB = Lock() # 创建Lock对象

class MyThread(Thread): # 自定义一个类,必须继承Thread
    def run(self):      # 自定义一个实例方法,名字必须是run
        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)
        #分析:当线程1执行完func1,然后执行到这里的时候,拿到了B锁,线程2执行func1的时候拿到了A锁,那么线程2还要继续执行func1里面的代码,再去拿B锁的时候,发现B锁被人拿了,那么就一直等着别人把B锁释放,那么就一直等着,等到线程1的sleep时间用完之后,线程1继续执行func2,需要拿A锁了,但是A锁被线程2拿着呢,还没有释放,因为他在等着B锁被释放,那么这俩人就尴尬了,你拿着我的老A,我拿着你的B,这就尴尬了,俩人就停在了原地
        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.

RLock内部维护着一个Lock和一个counter变量. counter记录了acquire的次数,从而使得资源可以被多次require. 直到一个线程所有的acquire都被release, 其他的线程才能获得资源. 上面的例子(死锁现象)如果使用RLock代替Lock, 则不会发生死锁:

from threading import RLock as Lock    # 从RLock中导入Lock

mutexA=Lock()    # 创建Lock对象

mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

下面举例:使用递归锁解决死锁现象

import time
from threading import Thread, Lock  # 引入Lock模块

class MyThread(Thread):

    def __init__(self, lockA, lockB):
        super().__init__()
        self.lockA = lockA
        self.lockB = lockB
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        self.lockA.acquire()
        print("f1拿到了A锁")
        self.lockB.acquire()
        print("f1拿到了B锁")
        self.lockB.release()
        self.lockA.release()
    def f2(self):
        self.lockB.acquire()
        print("f2拿到了B锁")
        time.sleep(2)
        self.lockA.acquire()
        print("f2拿到了A锁")
        self.lockA.release()
        self.lockB.release()

if __name__ == '__main__':
    lockA = Lock()  # 创建A锁
    lockB = Lock()  # 创建B锁
    t1 = MyThread(lockA, lockB) # 创建线程1
    t1.start()      # 启动线程1
    t2 = MyThread(lockA, lockB) # 创建线程2
    t2.start()      # 启动线程2
    print("主线程结束")

# 执行结果:
# f1拿到了A锁
# f1拿到了B锁
# f2拿到了B锁
# f1拿到了A锁
# 主线程结束

# 分析最后的执行结果,我们发现,按照主线程的代码流程,当我们启动线程1时,线程1中的f1首先拿到A锁和B锁,f2拿到B锁后开始sleep(2); 此时线程2几乎与线程1同时启动,线程2中的f1迅速拿到A锁. 注意,线程2中的f1又想要拿B锁时,却发现此时的B锁已经被线程1中的f2拿到了,线程1中的f2正处于sleep(2)的过程中. 于是线程2中的f1阻塞住. 当线程1中的f2结束sleep(2)后,按照代码流程它应该拿A锁了,但是却发现A锁被线程2拿到并阻塞住,因此线程1中的f2也只能阻塞住.如此一来,我们最终发现, 线程1中的f2拿着B锁阻塞着, 线程2中的f1拿着A锁阻塞着, 由此产生死锁现象!
死锁现象
import time
from threading import Thread, RLock  # 引入递归锁RLock

class MyThread(Thread):
    def __init__(self, lockA, lockB):
        super().__init__()
        self.lockA = lockA
        self.lockB = lockB
    def run(self):
        self.f1()
        self.f2()
    def f1(self):
        self.lockA.acquire()
        print("f1拿到了A锁")
        self.lockB.acquire()
        print("f1拿到了B锁")
        self.lockB.release()
        self.lockA.release()
    def f2(self):
        self.lockB.acquire()
        print("f2拿到了B锁")
        time.sleep(2)
        self.lockA.acquire()
        print("f2拿到了A锁")
        self.lockA.release()
        self.lockB.release()

if __name__ == '__main__':
    lockA = lockB = RLock()
    t1 = MyThread(lockA, lockB)
    t1.start()
    t2 = MyThread(lockA, lockB)
    t2.start()
    print("主线程结束")

# 执行结果:
# f1拿到了A锁
# f1拿到了B锁
# f2拿到了B锁
# 主线程结束
# f2拿到了A锁
# f1拿到了A锁
# f1拿到了B锁
# f2拿到了B锁
# f2拿到了A锁

# 最后打印出所有结果后,程序正常结束,没有出现死锁现象(阻塞住)
递归锁解决死锁现象

总结: 当我们的程序中需要两把锁的时候, 如果想要避免出现死锁现象, 最好使用递归锁.

posted @ 2018-10-26 21:45  咕噜噜~  阅读(495)  评论(0编辑  收藏  举报