5 - 线程 - Windows 10 - Python 的 5 种线程锁 - 控制线程切换

发布时间 2023-03-24 16:51:45作者: Loki_Severus

@

测试环境:

操作系统: Window 10
工具:Pycharm
Python: 3.7

一、线程安全

一个程序运行,指的是运行一个可执行文件,这里我们是介绍python,就指py脚本文件吧。
在运行py文件的过程中,系统为了执行这个py脚本文件,会为这个py脚本文件开一个进程,也就是赋予了它一部分系统资源,让它能够创建主线程,让主线程运行起来,假如我们在这个py脚本文件内,写入创建子线程的代码,那么在这py脚本文件运行时,会创建一个子线程,而这个子线程与主线程是共享数据的,也就是说主线程的数据会被子线程共享,这里的数据通常为主线程代码里的全局变量,假如py脚本文件内有两个子线程和一个主线程,那么在运行脚本时,其中一个子线程会在完整的执行一次后,交出GIL,让另外的线程运行,这里的线程指的是主线程或另外一个子线程,就是在这样的交替执行的规则下,以及由于线程的共享数据的原因(同一个进程所创建的线程,共享数据),主线程的数据很不安全,可以被子线程们任意修改,如果子线程指定的方法有调用到主线程代码内的全局变量的话。

想要解决这个问题就必须通过锁来保障线程切换的时机。
需要我们值得留意的是,Python基本数据类型中listtupledict本身就是属于线程安全的,所以如果有多个线程对这3种容器做操作时,我们不必考虑线程安全问题

Tips

说道这里还得再说一下,其实如果能够看透一些,就应该知道,我们的程序是由很多代码组成的,而这些代码运行后在现实世界里,因为一些现实的原因会有着一些变化,比如说对面可能无法很快处理我的请求request,服务器响应差等等原因,可能会出现延时,而一个脚本文件或者说一个主线程,在一开始就是独自运行全部的代码,但是呢?如果主线程突然执行到一个在现实世界会反馈一个延时的代码过程呢?这个过程难道要等待下去,其他代码不给执行了吗?就好像用电饭煲煮饭和洗澡不冲突,洗澡和用洗衣机洗衣服不冲突一样,其他的事情先有个开头,后面就利用CPU处理线程的光速度(处理一次线程需要的时间仅仅是几毫秒,1秒 = 1000毫秒)极速处理好第二个线程,第三个... 等到第一个线程延时过去了,可以加入GIL抢占的活动中,完成剩下的代码执行或程序。线程将一个代码文件内代码执行划分成一个又一个的任务,分段式执行 / 分段式任务,将所有的代码分段,一段时间就执行一段代码,这个过程是随机执行的 —— 线程是随机执行的。

前面说的太多话了,回归正传。

二、线程的切换

前面提到过线程的运行是随机的,所以假如我们想要自行控制线程的运行呢?即控制线程的切换,就需要用到线程锁这一手段了。

锁是Python提供给我们能够自行操控线程切换的一种手段,使用锁可以让线程的切换变的有序。
一旦线程的切换变的有序后,各个线程之间对数据的访问、修改就变的可控,所以若要保证线程安全,就必须使用锁。

threading模块中提供了5种最常见的锁,下面是按照功能进行划分:

  1. 同步锁:lock(一次只能放行一个)
  2. 递归锁:rlock(一次只能放行一个) recursion lock
  3. 条件锁:condition(一次可以放行任意个)
  4. 事件锁:event(一次全部放行)(红绿灯锁)
  5. 信号量锁:semaphore(一次可以放行特定个)

三、线程锁的基础核心构成总结

划重点 —————————————————————————————————

同步锁是线程锁的原始基础 ————————————|
递归锁是在同步锁的基础上做出来的 ————————||
条件锁是在递归锁的基础之上做出来的 ———————|||
事件锁是在条件锁的基础之上做出来的 ———————||||
信号锁也是在条件锁的基础之上做出来的 ——————||||

————————————————————————————————————

1. Lock() 同步锁

Lock锁的称呼有很多,如:
同步锁
互斥锁

它们是什么意思呢?如下所示:

互斥指的是某一资源同一时刻仅能有一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,即访问是无序的

同步是指在互斥的基础上(大多数情况),通过其他机制实现访问者对资源的有序访问
同步其实已经实现了互斥,是互斥的一种更为复杂的实现,因为它在互斥的基础上实现了有序访问的特点

说到同步,就要提到同步线程了,实际上Python的多线程是默认加互斥锁的,为什么呢?要知道这一点其实得知道 Cpython 解释器,这是目前最主流的python解释器
参考链接:
2 - 进程与线程 - CPython 解释器 - 多线程并行(实际并发)

如果清楚Cpython 解释器,也就应该清楚 Cpython解释器的GIL (Global Interpreter Lock,全局解释器锁),锁死了同一个进程内线程并行的可能性,只能同一个进程内的线程,一次只能一个线程进入CPU执行。所以Cpython 解释器的GIL锁,其实算是一个互斥锁了,不过它不限制顺序,即只要是同一个进程内的线程(主线程/子线程)都可以访问程序内的变量等等,也就是线程安全问题, 所以这里就需要一个同步的手段来限制同一个进程内的多个线程访问程序内的同一变量(例如全局变量),以此来解决线程安全问题。

所以加了锁的,都是为了一个线程安全问题,也就是要实现线程同步。

同步线程:
要实现线程 1 没结束对某个变量的访问 ,在线程 1 还没结束对那个变量的访问前,也就是同步锁释放前,无法切换线程 —— 即加了锁还没释放锁,所以线程们等待锁的释放。

引用:

同步锁一次只能放行一个线程,一个被加锁的线程在运行时不会将执行权交出去,只有当该线程被解锁时,才会将执行权通过系统调度交由其他线程。

线程锁 - 同步锁的实现方法

方法 描述
threading.Lock() 返回一个同步锁对象
lockObject.acquire() 上锁,当一个线程在执行被上锁代码块时,将不允许切换到其他线程运行,默认锁失效时间为1秒
lockObject.release() 解锁,当一个线程在执行未被上锁代码块时,将允许系统根据策略自行切换到其他线程中运行

代码演示:

import threading

num = 0

def add():
    lock.acquire()
    global num
    for i in range(1_0):  # Tips:注意在 range 方法内的参数可以是 下划线作为间隔的,不影响数字,下划线会被忽略
        num += 1
        print("add: ",num)
    lock.release()


def sub():
    lock.acquire()
    global num
    for i in range(1_0):  # Tips:注意在 range 方法内的参数可以是 下划线作为间隔的,不影响数字,下划线会被忽略
        num -= 1
        print("sub: ",num)
    lock.release()

if __name__ == "__main__":
    lock = threading.Lock()

    test_thread01 = threading.Thread(target=add)
    test_thread02 = threading.Thread(target=sub)
    
    test_thread01.start()
    test_thread02.start()

    test_thread01.join()
    test_thread02.join()

    print("num result : %s" % num)
    

运行结果:
在这里插入图片描述

按道理,python多线程(主线程与子线程)会交替执行的,但是被同步锁锁住了,里面包含着的全局变量num,也跟着被锁住了,无法被其他线程访问 —— 想要访问这个变量的线程都被暂停了,只能等待锁被释放。

那么本文的主旨是线程的切换,都有点跑题了,看到下面的代码和运行结果了吗?能够看到 start() 方法的位置了吗?02 的start()被变换了位置,当第一个线程的同步锁被释放了,那么第二个start() 时,就获得了前一个线程的同步锁。那么所谓的同步锁线程切换,是基于 start() 方法的位置。

代码演示:

import threading

num = 0

def add():
    lock.acquire()	# 上同步锁
    global num
    for i in range(1_0):  # Tips:注意在 range 方法内的参数可以是 下划线作为间隔的,不影响数字,下划线会被忽略
        num += 1
        print("add: ",num)
    lock.release()  # 解同步锁


def sub():
    lock.acquire() 		# 上同步锁
    global num
    for i in range(1_0):  # Tips:注意在 range 方法内的参数可以是 下划线作为间隔的,不影响数字,下划线会被忽略
        num -= 1
        print("sub: ",num)
    lock.release()		# 解同步锁

if __name__ == "__main__":
    lock = threading.Lock()		# 同步锁对象

    test_thread01 = threading.Thread(target=add)
    test_thread02 = threading.Thread(target=sub)
    
    test_thread02.start()  # 02 的start() 更改位置
    test_thread01.start()
    #test_thread02.start()  # 02 的start() 原始位置

    test_thread01.join()	# 堵塞主线程
    test_thread02.join()	# 堵塞主线程

    print("num result : %s" % num)

with 关键字 - 上下文管理器 - 同步锁

由于threading.Lock()对象中实现了enter__()__exit()方法,故我们可以使用with语句进行上下文管理形式的加锁解锁操作:

代码演示:

import threading

num = 0

def add():
     with lock:
        # 自动加锁
        global num
        for i in range(1_0):  # Tips:注意在 range 方法内的参数可以是 下划线作为间隔的,不影响数字,下划线会被忽略
            num += 1
            print("add: ",num)
    	# with 上下文管理器 自动解锁


def sub():
     with lock:
        # with 上下文管理器 自动加锁
        global num
        for i in range(1_0):  # Tips:注意在 range 方法内的参数可以是 下划线作为间隔的,不影响数字,下划线会被忽略
            num -= 1
            print("sub: ",num)
		# with 上下文管理器 自动解锁
		
if __name__ == "__main__":
    lock = threading.Lock()		# 同步锁对象

    test_thread01 = threading.Thread(target=add)
    test_thread02 = threading.Thread(target=sub)
    
    test_thread01.start()
    test_thread02.start()

    test_thread01.join()	# 堵塞主线程
    test_thread02.join()	# 堵塞主线程

    print("num result : %s" % num)

死锁现象

对于同步锁来说,一次acquire()必须对应一次release(),不能出现连续重复使用多次acquire()后再重复使用多次release()的操作,这样会引起死锁造成程序的阻塞,完全不动了,如下所示:

代码演示:

import threading

num = 0


def add():
    lock.acquire()  # 上同步锁
    lock.acquire()  # 死锁
    # 不执行
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()
    lock.release()


def sub():
    lock.acquire()  # 上同步锁
    lock.acquire()  # 死锁
    # 不执行
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()
    lock.release()


if __name__ == "__main__":
    lock = threading.Lock()		# 同步锁对象

    test_thread01 = threading.Thread(target=add)
    test_thread02 = threading.Thread(target=sub)

    test_thread01.start()
    test_thread02.start()

    test_thread01.join()    # 堵塞主线程
    test_thread02.join()	# 堵塞主线程

    print("num result : %s" % num)

运行结果:
在这里插入图片描述
卡死在这里没反应,也没法退出运行,只能将终端关闭了。

2. RLock() 递归锁

简介

递归锁是同步锁的一个升级版本,在同步锁的基础上可以做到连续重复使用多次acquire()后再重复使用多次release()的操作,但是一定要注意加锁次数和解锁次数必须一致,否则也将引发死锁现象。

线程锁 - 递归锁的实现方法

方法 描述
threading.RLock() 返回一个递归锁对象
lockObject.acquire(blocking=True,timeout=1) 上锁,当一个线程在执行被上锁代码块时,将不允许切换到其他线程运行,默认锁失效时间为1秒
lockObject.release() 解锁,当一个线程在执行未被上锁代码块时,将允许系统根据策略自行切换到其他线程中运行
lockObject.locaked() 判断该锁对象是否处于上锁状态,返回一个布尔值

以下是递归锁的简单使用,下面这段操作如果使用同步锁则会发生死锁现象,但是递归锁不会。

代码演示:

import threading

num = 0

def add():
    lock.acquire()  # 上递归锁
    lock.acquire()	# 在这里同步锁会变成死锁,递归锁却不会
    global num    
    for i in range(1_0):
        num += 1
        print("add: ",num)
    lock.release()	# 只要调用和 acquire() 上锁方法同等数量的 解锁方法 release()
    lock.release()


def sub():
    lock.acquire()   # 上递归锁
    lock.acquire()   # 在这里同步锁会变成死锁,递归锁却不会
    global num		 
    for i in range(1_0):
        num -= 1
        print("sub: ",num)
    lock.release()  # 只要调用和 acquire() 上锁方法同等数量的 解锁方法 release()
    lock.release()


if __name__ == "__main__":
    lock = threading.RLock()  # 递归锁对象

    test_thread01 = threading.Thread(target=add)
    test_thread02 = threading.Thread(target=sub)

    test_thread01.start()
    test_thread02.start()

    test_thread01.join()	# 堵塞主线程
    test_thread02.join()	# 堵塞主线程

    print("num result : %s" % num)

运行结果:
在这里插入图片描述

with 关键字 - 上下文管理器 - 递归锁

由于threading.RLock()对象中实现了enter__()__exit()方法,故我们可以使用with语句进行上下文管理形式的加锁解锁操作:

代码演示:

import threading

num = 0


def add():
    with lock:
        # 自动加锁
        global num
        for i in range(1_0):
            num += 1
            print("add: ",num)
        # 自动解锁


def sub():
    with lock:
        # 自动加锁
        global num
        for i in range(1_0):
            num -= 1
            print("sub: ",num)
        # 自动解锁


if __name__ == "__main__":
    lock = threading.RLock()  # 递归锁对象

    test_thread01 = threading.Thread(target=add)
    test_thread02 = threading.Thread(target=sub)

    test_thread01.start()
    test_thread02.start()

    test_thread01.join()	# 堵塞主线程
    test_thread02.join()	# 堵塞主线程

    print("num result : %s" % num)

运行结果:
在这里插入图片描述

3. Condition() 条件锁

条件锁在递归锁的基础上增加了能够暂停线程运行的功能。并且我们可以使用wait()notify()来控制线程执行的个数。

注意这里是在递归锁的基础上

注意:条件锁可以自由设定一次放行几个线程。

————————————————————————————————————————

特别注意:

条件锁调用 —— 必须上递归锁的 wait()notify()

————————————————————————————————————————

线程锁 - 条件锁的实现方法

方法 描述
threading.Condition() 返回一个条件锁对象
lockObject.acquire(blocking=True,timeout=1) 上锁,当一个线程在执行被上锁代码块时,将不允许切换到其他线程运行,默认锁失效时间为1秒
lockObject.release() 解锁,当一个线程在执行未被上锁代码块时,将允许系统根据策略自行切换到其他线程中运行
lockObject.wait(timeout=None) 将当前线程设置为“等待”状态,只有该线程接到“"通知”或者超时时间到期之后才会继续运行,在“等待”状态下的线程将允许系统根据策路自行切换到其他线程中运行
lockObject.wait for(predicate,timeout=None) 将当前线程设置为“等待”状态,只有该线程的即predicate返回一个True或者超时时间到期之后才会继续运行,在“等待”状态下的线程将允许系统根据策略自行切换到其他线程中运行。注意:predicate参数应当传入一个可调用对象,且返回结果为bool类型
lockObject.notify(n=1) 通知一个当前状态为"等待”的线程继续运行,也可以通过参数 n通知多个
lockObject.notify_all() 通知所有当前状态为“等待”的线程继续运行

代码演示:

import threading

currentRunThreadNumber = 0		# 当前正在运行中的线程数量
maxSubThreadNumber = 10   # 设置启动 10 个线程


def task():
    global currentRunThreadNumber
    thName = threading.currentThread().name

    condLock.acquire()  # 递归锁 上锁
    print("start and wait run thread : %s" % thName)

    condLock.wait()  # 暂停线程运行、等待唤醒,同步锁的无法切换线程在这里是没用的
    currentRunThreadNumber += 1
    print("carry on run thread : %s" % thName)

    condLock.release()  # 递归锁 解锁


if __name__ == "__main__":
    condLock = threading.Condition()  # 条件锁对象

    for i in range(maxSubThreadNumber):
        test_thread= threading.Thread(target=task)
        test_thread.start()

    while currentRunThreadNumber < maxSubThreadNumber:
            notifyNumber = int(input("Please enter the number of threads that need to be notified to run:"))

            condLock.acquire()         # 获取线程锁,以便切换
            condLock.notify(notifyNumber)  # 放行,指定数量的线程去自由切换
            condLock.release()          # 释放锁,给放行的线程使用

    print("main thread run end")

这里非常意思的地方在于前面启动了10线程的递归锁无法切换线程的限制被条件锁的 wait() 方法破除了,运行了 条件锁的 wait() 方法,即使没释放递归锁,也能切换其他线程,比如运行了 10 子线程,都被 wait() 方法设置成等待状态了,所以系统就切换回主线程,运行主线程的程序,不过在主线程这里给设置了递归锁,那么我认为这应该是将前面的递归锁给了主线程,锁只有一个(同步锁的特点),具有唯一性和排他性(互斥性),而且想要用条件锁的 notify()就必须上递归锁否则会报错,如下所示:

报错代码演示:

import threading

currentRunThreadNumber = 0		# 当前正在运行中的线程数量
maxSubThreadNumber = 10   # 设置启动 10 个线程


def task():
    global currentRunThreadNumber
    thName = threading.currentThread().name

    condLock.acquire()  # 递归锁 上锁
    print("start and wait run thread : %s" % thName)

    condLock.wait()  # 暂停线程运行、等待唤醒,同步锁的无法切换线程在这里是没用的
    currentRunThreadNumber += 1
    print("carry on run thread : %s" % thName)

    condLock.release()  # 递归锁 解锁


if __name__ == "__main__":
    condLock = threading.Condition() # 条件锁对象

    for i in range(maxSubThreadNumber):
        test_thread = threading.Thread(target=task)
        test_thread.start()

    while currentRunThreadNumber < maxSubThreadNumber:
            notifyNumber = int(input("Please enter the number of threads that need to be notified to run:"))

            #condLock.acquire()         # 获取线程锁,以便切换
            condLock.notify(notifyNumber)  # 放行,指定数量的线程去自由切换
            #condLock.release()          # 释放锁,给放行的线程使用

    print("main thread run end")

运行结果:
在这里插入图片描述

条件变量比较关键的接口解析如上所述,实际上是包装了一个锁对象,可以是原始锁Lock或者是可重入锁RLock,多个线程通过共用一把锁来进行身份控制(主从),当有线程对其锁对象上锁后则其余线程对此条件变量就只有wait方法(只能通过 wait() 方法切换线程了)。

参考链接:python中lock锁和阻塞_Python的锁源码剖析

with 关键字 - 上下文管理器 - 条件锁

由于threading.Condition()对象中实现了enter__()__exit()方法,故我们可以使用with语句进行上下文管理形式的加锁解锁操作:

import threading

currentRunThreadNumber = 0      # 当前正在运行中的线程数量
maxSubThreadNumber = 10         # 设置启动 10 个线程


def task():
    global currentRunThreadNumber
    thName = threading.currentThread().name

    with condLock:      # 条件锁 自动上锁
        print("start and wait run thread : %s" % thName)
        condLock.wait()  # 暂停线程运行、等待唤醒
        currentRunThreadNumber += 1
        print("carry on run thread : %s" % thName)
        				 # 条件锁 自动解锁

if __name__ == "__main__":
    condLock = threading.Condition() # 条件锁对象

    for i in range(maxSubThreadNumber):
        test_thread = threading.Thread(target=task)
        test_thread.start()

    while currentRunThreadNumber < maxSubThreadNumber:
        notifyNumber = int(
            input("Please enter the number of threads that need to be notified to run:"))

        with condLock:   # 条件锁 自动上锁
            condLock.notify(notifyNumber)  # 放行
                         # 条件锁 自动解锁


    print("main thread run end")

4. Event() 事件锁 (红绿灯锁)

基本介绍

事件锁是基于条件锁来做的,它与条件锁的区别在于一次只能放行全部,不能放行任意个数量的子线程继续运行。
我们可以将事件锁看为红绿灯,当红灯时所有子线程都暂停运行,并进入“等待”状态,当绿灯时所有子线程都恢复“运行”。

线程锁 - 事件锁的实现方法

方法 描述
threading.Event() 返回一个事件锁对象
lockObject.clear() 将事件锁设为红灯状态,即所有线程暂停运行
lockObject.is_set() 用来判断当前事件锁状态,红灯为False,绿灯为True
lockObject.set() 将事件锁设为绿灯状态,即所有线程恢复运行
lockObject.wait(timeout=None) 将当前线程设置为“等待”状态,只有该线程接到“绿灯通知”或者超时时间到期之后才会继续运行,在“等待”状态下的线程将允许系统根据策略自行切换到其他线程中运行

事件锁没法使用 with 关键字 - 上下文管理器

事件锁不能利用with语句来进行使用,只能按照常规方式。
如下所示,我们来模拟线程和红绿灯的操作,红灯停,绿灯行:

import threading
import time

maxSubThreadNumber = 3


def task():
    thName = threading.currentThread().name
    print("start and wait run thread : %s" % thName)  
    print(eventLock.is_set())   # 绿灯为 True 红灯为 False
    eventLock.wait()  # 暂停当前线程的运行,等待绿灯 切换线程          
    print(eventLock.is_set())   # 绿灯为 True 红灯为 False
    print("green light 1, %s carry on run" % thName)   
    eventLock.wait()  # 暂停当前线程运行,等待绿灯      
    print(eventLock.is_set())   # 绿灯为 True 红灯为 False
    print("red light, %s " % thName)
    
    print("green light 2, %s carry on run" % thName)
    #print("sub thread %s run end" % thName)


if __name__ == "__main__":

    eventLock = threading.Event()

    for i in range(maxSubThreadNumber):
        test_thread = threading.Thread(target=task)
        test_thread.start()
    
    eventLock.set()  # 所有线程设置为绿灯    第一个 set() 方法 first
    #time.sleep(3)
    eventLock.clear()  # 所有线程设置为红灯,暂停运行
    #time.sleep(3)
    eventLock.set()  # 所有线程设置为绿灯   第二个 set() 方法  second
 
    

运行结果分析:
结果 1

D:\PythonProject>python test4.py
start and wait run thread : Thread-1
False
start and wait run thread : Thread-2
False
start and wait run thread : Thread-3
True
green light 1, Thread-1 carry on run
True
True
green light 1, Thread-3 carry on run
True
red light, Thread-3
green light 2, Thread-3 carry on run
True
True
red light, Thread-1
green light 2, Thread-1 carry on run
green light 1, Thread-2 carry on run
True
red light, Thread-2
green light 2, Thread-2 carry on run

结果 2

D:\PythonProject>python test4.py
start and wait run thread : Thread-1
False
start and wait run thread : Thread-2
start and wait run thread : Thread-3
True
True
green light 1, Thread-3 carry on run
True
True
True
green light 1, Thread-2 carry on run
green light 1, Thread-1 carry on run
True
red light, Thread-2
True
green light 2, Thread-2 carry on run
red light, Thread-3
green light 2, Thread-3 carry on run
True
red light, Thread-1
green light 2, Thread-1 carry on run

是不是看不懂,我也分不清,因为多线程的执行很唯心,你猜不到当前的线程在遇到事件锁的 wait() 方法时,它会切换到哪一个线程,有可能是主线程,也有可能是其他的子线程,所以在这里如果是切换回主线程,那么直接就是执行主线程代码的第一个 set() 方法 ,也就是向等待的线程们发送绿灯信号,可以运行了,而这就要看被切换到的线程是怎么切换的,如果是切换到另外的子线程,那就执行子线程的代码,如print之类的输出,然后又遇到 事件锁的 wait() 方法,又得切换线程,所以这个很让人蒙圈的,你猜不到的哪个线程被切换到,这里主线程和子线程都有可能,还有最有意思的是在主线程的代码里有一个事件锁的 clear() 方法,在我看来,它不是暂停所有的线程,应该是只暂停其他的线程,而不切换当前的线程,并且还给当前线程一路绿灯,直接运行当前切换到的线程,一直到结束为止。所以这被切换到的线程,简直可以说是天命之子啊!这一点我是运行了好几次,看结果推断出来的,具体的可以自己推理下。

5. Semaphore() 信号量锁

基本介绍

信号量锁也是根据条件锁来做的,它与条件锁和事件锁的区别如下:

条件锁:一次可以放行任意个处于“等待”状态的线程 —— 可以指定一定数量的线程运行
事件锁:一次可以放行全部的处于“等待”状态的线程 —— 无法指定一定数量的线程运行
信号量锁:通过规定,成批的放行特定个处于“上锁”状态的线程

线程锁 - 信号锁的实现方法

方法 描述
threading.Semaphore() 返回一个信号量锁对象,可以指定放行的线程数量,和条件锁类似
lockObject.acquire(blocking=True,timeout=1) 上锁,当一个线程在执行被上锁代码块时,将不允许切换到其他线程运行,默认锁失效时问为1秒
lockObject.release() 解锁,当一个线程在执行未被上锁代码块时,将允许系统根据策路自行切换到其他线程中运行

代码演示:

import threading
import time

maxSubThreadNumber = 6


def task():
    thName = threading.currentThread().name
    semaLock.acquire()
    print("run sub thread %s" % thName)
    time.sleep(3)   # 线程睡眠,会随机切换另外的线程执行
    semaLock.release()


if __name__ == "__main__":
    # 每次只能放行2个
    semaLock = threading.Semaphore(2) #创建信号锁对象,并且执行放行的线程数量为 2个

    for i in range(maxSubThreadNumber):
        test_thread = threading.Thread(target=task)
        test_thread.start()

运行结果:

每次放行 2 个线程并且在第一个线程sleep线程睡眠停止 3 秒后,要了解一点是,放行两个线程第一个线程睡眠sleep后,是肯定要切换线程的,但是这个线程切换的时间太快了,按道理会有很小很小的时间间隔,这两线程之间的 sleep 线程睡眠 3 秒其实应该不是同步的,但是对于人类来说,小于毫秒的其实都是一样的,反正人类的反应能力没那么快,所以就当同时好了。信号锁规定了放行的线程数量,所以即使切换到第二个线程,并执行到第二个线程 sleep线程睡眠方法时,线程不会继续切换到第三个线程去执行,而是会等待设置的线程睡眠时间过了,才会继续切换线程,也就是第三和第四的线程。
在这里插入图片描述

with 关键字 - 上下文管理器 - 条件锁

由于threading.Semaphore()对象中实现了enter__()__exit()方法,故我们可以使用with语句进行上下文管理形式的加锁解锁操作:

import threading
import time

maxSubThreadNumber = 6


def task():
    thName = threading.currentThread().name
    with semaLock:  # 信号锁 自动上锁
        print("run sub thread %s" % thName)
        time.sleep(3)	# 线程睡眠,会随机切换另外的线程执行
					# 信号锁 自动解锁

if __name__ == "__main__":

    semaLock = threading.Semaphore(2) #创建信号锁对象,并且执行放行的线程数量为 2个

    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()

四、锁关系浅析

上面5种锁可以说都是基于同步锁来做的,这些你都可以从源码中找到答案。

RLock() 递归锁源码解析

首先来看RLock递归锁,递归锁的实现非常简单,它的内部会维护着一个计数器,当计数器不为0的时候该线程不能被I/O操作和时间轮询机制切换。但是当计数器为0的时候便不会如此了:

def __init__(self):
    self._block = _allocate_lock()
    self._owner = None
    self._count = 0  # 计数器

Condition条件锁的内部其实是有两把锁的,一把底层锁同步锁)一把高级锁(递归锁)。

低层锁的解锁方式有两种,使用wait()方法会暂时解开底层锁同时加上一把高级锁,只有当接收到别的线程里的notfiy()后才会解开高级锁和重新上锁低层锁,也就是说条件锁底层是根据同步锁和递归锁的不断切换来进行实现的

要知道同步锁是不可以切换线程的,那么当使用了 wait() 方法时,存在了同步锁锁住了线程,无法切换线程,可是依旧是可以切换线程,所以当使用了 wait() 方法时,是把底层锁(同步锁)切换为高级锁(递归锁),所以上了锁的线程才能切换到另外的线程去执行,在这里递归锁可以切换线程执行,关键在于wait() ,将底层锁与高级锁的切换。仅供参考。

Condition() 条件锁源码解析

def __init__(self, lock=None):
    if lock is None:
        lock = RLock()  # 可以看到条件锁的内部是基于递归锁,而递归锁又是基于同步锁
    self._lock = lock

    self.acquire = lock.acquire
    self.release = lock.release
    try:
        self._release_save = lock._release_save
    except AttributeError:
        pass
    try:
        self._acquire_restore = lock._acquire_restore
    except AttributeError:
        pass
    try:
        self._is_owned = lock._is_owned
    except AttributeError:
        pass
    self._waiters = _deque()

Event() 事件锁源码解析

Event事件锁内部是基于条件锁来做的:

class Event:

    def __init__(self):
        self._cond = Condition(Lock())  # 实例化出了一个条件锁,
        self._flag = False				# 条件锁又实例化同步锁做参数

    def _reset_internal_locks(self):
        # private!  called by Thread._reset_internal_locks by _after_fork()
        self._cond.__init__(Lock())

    def is_set(self):
        """Return true if and only if the internal flag is true."""
        return self._flag

    isSet = is_set    # 在这里 isSet 相当于 is_set() 方法,没有区别

同步锁是递归锁的基础,而递归锁是条件锁的基础,事件锁以条件锁为基础,可看到条件锁的源码,当lock是非空时,那么就会传递一个同步锁实例作为初始化参数,所以可以知道事件锁不是直接将条件锁实例化后,又实例化一个递归锁,而是传递一个底层锁(同步锁),作为底层锁,因为前面的递归锁作为了条件锁的基础,其相关有用的代码,已经是条件锁的基石了,所以事件锁的源码内直接传递底层锁(同步锁)实例可以更方便些,也不用重复代码。仅供参考。

Semaphore() 信号锁源码解析

Semaphore信号量锁内部也是基于条件锁来做的:

class Semaphore:

    def __init__(self, value=1):
        if value < 0:
            raise ValueError("semaphore initial value must be >= 0")
        self._cond = Condition(Lock()) # 可以看到,这里是实例化出了一个条件锁
        self._value = value

和事件锁的源码相似,也是直接传递底层锁(同步锁)作为初始化参数,无需重复代码,也更方便些。