首页 > 编程语言 >python5种线程锁

python5种线程锁

时间:2022-09-29 17:15:04浏览次数:42  
标签:__ Thread lock threading num 线程 python5

# 线程安全

线程安全是多线程或多进程编程中的一个概念,在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

线程安全的问题最主要还是由线程切换导致的,比如一个房间(进程)中有10颗糖(资源),除此之外还有3个小人(1个主线程、2个子线程),当小人A吃了3颗糖后被系统强制进行休息时他认为还剩下7颗糖,而当小人B工作后又吃掉了3颗糖,那么当小人A重新上岗时会认为糖还剩下7颗,但是实际上只有4颗了。

上述例子中线程A和线程B的数据不同步,这就是线程安全问题,它可能导致非常严重的意外情况发生,我们按下面这个示例来进行说明。

下面有一个数值num初始值为0,我们开启2条线程:

  • 线程1对num进行一千万次+1的操作

  • 线程2对num进行一千万次-1的操作

结果可能会令人咋舌,num最后并不是我们所想象的结果0:

 1 import threading
 2 
 3 num = 0
 4 
 5 
 6 def add():
 7     global num
 8     for i in range(10_000_000):
 9         num += 1
10 
11 
12 def sub():
13     global num
14     for i in range(10_000_000):
15         num -= 1
16 
17 
18 if __name__ == "__main__":
19     subThread01 = threading.Thread(target=add)
20     subThread02 = threading.Thread(target=sub)
21 
22     subThread01.start()
23     subThread02.start()
24 
25     subThread01.join()
26     subThread02.join()
27 
28     print("num result : %s" % num)
29 
30 # 结果三次采集
31 # num result : 669214
32 # num result : -1849179
33 # num result : -525674

 

上面这就是一个非常好的案例,想要解决这个问题就必须通过锁来保障线程切换的时机。

需要我们值得留意的是,在Python基本数据类型中list、tuple、dict本身就是属于线程安全的,

所以如果有多个线程对这3种容器做操作时,我们不必考虑线程安全问题。

# 锁的作用

锁是Python提供给我们能够自行操控线程切换的一种手段,使用锁可以让线程的切换变的有序。

一旦线程的切换变的有序后,各个线程之间对数据的访问、修改就变的可控,所以若要保证线程安全,就必须使用锁。

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

  • 同步锁:lock(一次只能放行一个)

  • 递归锁:rlock(一次只能放行一个)

  • 条件锁:condition(一次可以放行任意个)

  • 事件锁:event(一次全部放行)

  • 信号量锁:semaphore(一次可以放行特定个)

# 1、Lock() 同步锁

基本介绍

Lock锁的称呼有很多,如:

  • 同步锁

  • 互斥锁

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

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

  2. 同步是指在互斥的基础上(大多数情况),通过其他机制实现访问者对资源的有序访问

  3. 同步其实已经实现了互斥,是互斥的一种更为复杂的实现,因为它在互斥的基础上实现了有序访问的特点

下面是threading模块与同步锁提供的相关方法:

使用方式

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

如下所示,使用同步锁解决最上面的问题:

import threading

num = 0

def add():
    lock.acquire()
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()


def sub():
    lock.acquire()
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()

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

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

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

# 结果三次采集
# num result : 0
# num result : 0
# num result : 0

 

这样这个代码就完全变成了串行的状态,对于这种计算密集型I/O业务来说,还不如直接使用串行化单线程执行来得快,所以这个例子仅作为一个示例,不能概述锁真正的用途。

死锁现象

对于同步锁来说,一次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()

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.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(10_000_000):
            num += 1
        # 自动解锁


def sub():
    with lock:
        # 自动加锁
        global num
        for i in range(10_000_000):
            num -= 1
        # 自动解锁


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

    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)

    subThread01.start()
    subThread02.start()

    subThread01.join()
    subThread02.join()

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

# 结果三次采集
# num result : 0
# num result : 0
# num result : 0

# 2、RLock() 递归锁

基本介绍

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

下面是threading模块与递归锁提供的相关方法:

使用方式

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

 1 import threading
 2 
 3 num = 0
 4 
 5 
 6 def add():
 7     lock.acquire()
 8     lock.acquire()
 9     global num
10     for i in range(10_000_000):
11         num += 1
12     lock.release()
13     lock.release()
14 
15 
16 def sub():
17     lock.acquire()
18     lock.acquire()
19     global num
20     for i in range(10_000_000):
21         num -= 1
22     lock.release()
23     lock.release()
24 
25 
26 if __name__ == "__main__":
27     lock = threading.RLock()
28 
29     subThread01 = threading.Thread(target=add)
30     subThread02 = threading.Thread(target=sub)
31 
32     subThread01.start()
33     subThread02.start()
34 
35     subThread01.join()
36     subThread02.join()
37 
38     print("num result : %s" % num)
39 
40 # 结果三次采集
41 # num result : 0
42 # num result : 0
43 # num result : 0

with语句

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

 1 import threading
 2 
 3 num = 0
 4 
 5 
 6 def add():
 7     with lock:
 8         # 自动加锁
 9         global num
10         for i in range(10_000_000):
11             num += 1
12         # 自动解锁
13 
14 
15 def sub():
16     with lock:
17         # 自动加锁
18         global num
19         for i in range(10_000_000):
20             num -= 1
21         # 自动解锁
22 
23 
24 if __name__ == "__main__":
25     lock = threading.RLock()
26 
27     subThread01 = threading.Thread(target=add)
28     subThread02 = threading.Thread(target=sub)
29 
30     subThread01.start()
31     subThread02.start()
32 
33     subThread01.join()
34     subThread02.join()
35 
36     print("num result : %s" % num)
37 
38 # 结果三次采集
39 # num result : 0
40 # num result : 0
41 # num result : 0

# 3、Condition() 条件锁

基本介绍

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

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

下面是threading模块与条件锁提供的相关方法:

使用方式

下面这个案例会启动10个子线程,并且会立即将10个子线程设置为等待状态。

然后我们可以发送一个或者多个通知,来恢复被等待的子线程继续运行:

import threading

currentRunThreadNumber = 0
maxSubThreadNumber = 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):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.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个子线程,然后这些子线程会全部变为等待状态
# start and wait run thread : Thread-1
# start and wait run thread : Thread-2
# start and wait run thread : Thread-3
# start and wait run thread : Thread-4
# start and wait run thread : Thread-5
# start and wait run thread : Thread-6
# start and wait run thread : Thread-7
# start and wait run thread : Thread-8
# start and wait run thread : Thread-9
# start and wait run thread : Thread-10

# 批量发送通知,放行特定数量的子线程继续运行
# Please enter the number of threads that need to be notified to run:5  # 放行5个
# carry on run thread : Thread-4
# carry on run thread : Thread-3
# carry on run thread : Thread-1
# carry on run thread : Thread-2
# carry on run thread : Thread-5

# Please enter the number of threads that need to be notified to run:5  # 放行5个
# carry on run thread : Thread-8
# carry on run thread : Thread-10
# carry on run thread : Thread-6
# carry on run thread : Thread-9
# carry on run thread : Thread-7

# Please enter the number of threads that need to be notified to run:1
# main thread run end

with语句

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

 1 import threading
 2 
 3 currentRunThreadNumber = 0
 4 maxSubThreadNumber = 10
 5 
 6 
 7 def task():
 8     global currentRunThreadNumber
 9     thName = threading.currentThread().name
10 
11     with condLock:
12         print("start and wait run thread : %s" % thName)
13         condLock.wait()  # 暂停线程运行、等待唤醒
14         currentRunThreadNumber += 1
15         print("carry on run thread : %s" % thName)
16 
17 
18 if __name__ == "__main__":
19     condLock = threading.Condition()
20 
21     for i in range(maxSubThreadNumber):
22         subThreadIns = threading.Thread(target=task)
23         subThreadIns.start()
24 
25     while currentRunThreadNumber < maxSubThreadNumber:
26         notifyNumber = int(
27             input("Please enter the number of threads that need to be notified to run:"))
28 
29         with condLock:
30             condLock.notify(notifyNumber)  # 放行
31 
32     print("main thread run end")

# 4、Event() 事件锁

基本介绍

事件锁是基于条件锁来做的,它与条件锁的区别在于一次只能放行全部,不能放行任意个数量的子线程继续运行。

我们可以将事件锁看为红绿灯,当红灯时所有子线程都暂停运行,并进入“等待”状态,当绿灯时所有子线程都恢复“运行”。

下面是threading模块与事件锁提供的相关方法:

使用方式

事件锁不能利用with语句来进行使用,只能按照常规方式。

如下所示,我们来模拟线程和红绿灯的操作,红灯停,绿灯行:

 1 # 生成一个事件锁对象
 2 eve = threading.Event()
 3 
 4 
 5 # 将事件锁设置为红灯状态
 6 eve.clear()
 7  
 8 # 判断事件锁的状态
 9 eve.is_set()
10 
11 
12 # 将当前线程设置’等待‘状态
13 eve.wait()
14 
15 
16 # 将事件锁设置为绿灯状态
17 eve.set()
18 
19 
20 import time
21 import threading
22 
23 
24 def light(eve):
25     print(f'当前时间:{time.ctime()}, 红灯还有 5s 结束!')
26     time.sleep(5)
27     print(f'当前时间:{time.ctime()}, 绿灯亮!')
28     eve.set()   # 设置事件锁标志为 True
29 
30 def car(eve, name):
31     print(f'当前时间:{time.ctime()}, 车 {name} 正在等红灯')
32     eve.wait()    # 将当前线程设置为等待状态,等待事件锁标志为 True 再执行
33     print(f'当前时间:{time.ctime()}, 车 {name} 开始通行')
34 
35 
36 
37 if __name__ == '__main__':
38     eve = threading.Event()   # 事件锁默认标志为 False
39     t1 = threading.Thread(target=light, args=(eve,))
40     t1.start()
41 
42     for each in 'ABCDE':
43         t2 = threading.Thread(target=car, args=(eve, each))
44         t2.start()
45         
46         
47         
48 # 执行看一下打印结果   
49 当前时间:Fri Jul 29 11:32:58 2022, 红灯还有 5s 结束!
50 当前时间:Fri Jul 29 11:32:58 2022, 车 A 正在等红灯
51 当前时间:Fri Jul 29 11:32:58 2022, 车 B 正在等红灯
52 当前时间:Fri Jul 29 11:32:58 2022, 车 C 正在等红灯
53 当前时间:Fri Jul 29 11:32:58 2022, 车 D 正在等红灯
54 当前时间:Fri Jul 29 11:32:58 2022, 车 E 正在等红灯
55 当前时间:Fri Jul 29 11:33:03 2022, 绿灯亮!
56 当前时间:Fri Jul 29 11:33:03 2022, 车 C 开始通行
57 当前时间:Fri Jul 29 11:33:03 2022, 车 B 开始通行
58 当前时间:Fri Jul 29 11:33:03 2022, 车 E 开始通行
59 当前时间:Fri Jul 29 11:33:03 2022, 车 A 开始通行
60 当前时间:Fri Jul 29 11:33:03 2022, 车 D 开始通行

# 5、Semaphore() 信号量锁

基本介绍

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

  • 条件锁:一次可以放行任意个处于“等待”状态的线程

  • 事件锁:一次可以放行全部的处于“等待”状态的线程

  • 信号量锁:通过规定,成批的放行特定个处于“上锁”状态的线程

下面是threading模块与信号量锁提供的相关方法:

使用方式

以下是使用示例,你可以将它当做一段限宽的路段,每次只能放行相同数量的线程:

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)

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


# run sub thread Thread-1
# run sub thread Thread-2

# run sub thread Thread-3
# run sub thread Thread-4

# run sub thread Thread-6
# run sub thread Thread-5

 

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)

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

# 锁关系浅析

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

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

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

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

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

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()
                            

# 基本练习题

条件锁的应用

需求:一个空列表,两个线程轮番往里面加值(一个加偶数,一个加奇数),最终让该列表中的值为 1 - 100 ,且是有序排列的。

 1 import threading
 2 
 3 lst = []
 4 
 5 
 6 def even():
 7     """加偶数"""
 8     with condLock:
 9         for i in range(2, 101, 2):
10             # 判断当前列表的长度处于2是否能处尽
11             # 如果能处尽则代表需要添加奇数
12             # 否则就添加偶数
13             if len(lst) % 2 != 0:
14                 # 添偶数
15                 lst.append(i)      # 先添加值
16                 condLock.notify()  # 告诉另一个线程,你可以加奇数了,但是这里不会立即交出执行权
17                 condLock.wait()    # 交出执行权,并等待另一个线程通知加偶数
18             else:
19                 # 添奇数
20                 condLock.wait()  # 交出执行权,等待另一个线程通知加偶数
21                 lst.append(i)    
22                 condLock.notify()
23         condLock.notify()
24 
25 
26 def odd():
27     """加奇数"""
28     with condLock:
29         for i in range(1, 101, 2):
30             if len(lst) % 2 == 0:
31                 lst.append(i)
32                 condLock.notify()
33                 condLock.wait()
34         condLock.notify()
35 
36 
37 if __name__ == "__main__":
38     condLock = threading.Condition()
39 
40     addEvenTask = threading.Thread(target=even)
41     addOddTask = threading.Thread(target=odd)
42 
43     addEvenTask.start()
44     addOddTask.start()
45 
46     addEvenTask.join()
47     addOddTask.join()
48 
49     print(lst)

事件锁的应用

有2个任务线程来扮演李白和杜甫,如何让他们一人一句进行对答?文本如下:

杜甫:老李啊,来喝酒!

李白:老杜啊,不喝了我喝不下了!

杜甫:老李啊,再来一壶?

杜甫:…老李?

李白:呼呼呼…睡着了..

代码如下:

import threading


def libai():
    event.wait()  
    print("李白:老杜啊,不喝了我喝不下了!")
    event.set()
    event.clear()
    event.wait()
    print("李白:呼呼呼...睡着了..")

def dufu():
    print("杜甫:老李啊,来喝酒!")
    event.set()  
    event.clear()
    event.wait()
    print("杜甫:老李啊,再来一壶?")
    print("杜甫:...老李?")
    event.set()


if __name__ == '__main__':

    event = threading.Event()

    t1 = threading.Thread(target=libai)
    t2 = threading.Thread(target=dufu)

    t1.start()
    t2.start()
    t1.join()
    t2.join()

 

标签:__,Thread,lock,threading,num,线程,python5
From: https://www.cnblogs.com/sunzhiqi/p/16742208.html

相关文章

  • python的多线程
    一、线程的概念线程是CPU分配资源的基本单位。当一程序开始运行,这个程序就变成了一个进程,而一个进程相当于一个或者多个线程。当没有多线程编程时,一个进程相当于一个主线程......
  • WPF 给 Pen 的 DashStyle 设置 0 0 的虚线数组将会让渲染线程消耗大量 CPU 资源
    给WPF的Pen的DashStyle属性设置00的虚线,在绘制几何图形时,绘制的几何图形的尺寸将关联渲染线程所使用的CPU资源。大约在周长大于500时,将可以从任务管理器上看......
  • WPF 应用启动过程同时启动多个 UI 线程且访问 ContentPresenter 可能让多个 UI 线程互
    在应用启动过程里,除了主UI线程之外,如果还多启动了新的UI线程,且此新的UI线程碰到ContentPresenter类型,那么将可能存在让新的UI线程和主UI线程互等。这是多线程......
  • Linux系统编程——线程同步与互斥:互斥锁
    为什么需要互斥锁?在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在......
  • Linux 线程浅析
    进程和线程的区别与联系在许多经典的操作系统教科书中,总是把进程定义为程序的执行实例,它并不执行什么,只是维护应用程序所需的各种资源,而线程则是真正的执行实体。为了让进......
  • 通过Thread Pool Executor类解析线程池执行任务的核心流程
    摘要:ThreadPoolExecutor是Java线程池中最核心的类之一,它能够保证线程池按照正常的业务逻辑执行任务,并通过原子方式更新线程池每个阶段的状态。本文分享自华为云社区《​​【......
  • 线程基础知识18 线程池
    1什么是线程池线程池其实就是一种多线程处理形式,把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。2为什么使......
  • python的进程池,线程池的使用
    进程池importmultiprocessingimporttime#多进程编程defget_html(n):time.sleep(n)print('subprogresssuccess')returnnif__name__=='__main__':......
  • Java中的异步与线程池 保姆级教学
    Java中的异步与线程池保姆级教学2022-09-2509:10·Java架构师鸨哥初始化线程的4种方式1、继承ThreadThread01thread01=newThread01(); thread01.start();......
  • Java线程同步的四种方式详解(建议收藏)
    ​ Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen目录什么是线程同步线程同步的几种方式......