前言
本章节继续探讨threading
模块下关于锁的应用,注意。这一期很重要,依然是围绕着理论篇来讲,这一章节主要围绕理论篇中的线程切换做讲解,因此一定要有一些线程切换方面的知识。
官方中文文档
线程安全
线程安全是多线程编程时的计算机程序代码中的一个概念。在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
在聊线程安全之前,我还是决定拿之前的那个例子来描述它。
这里关于线程安全的代码测试我决定不用上面例举的这个例子,而是用多线程做密集型计算做演示,数值越大效果越明显:
import threading num = 0 def add(): global num for i in range(10000000): # 一千万次 num += 1 def sub(): global num for i in range(10000000): # 一千万次 num -= 1 if __name__ == '__main__': t1 = threading.Thread(target=add,) t2 = threading.Thread(target=sub,) t1.start() t2.start() t1.join() t2.join() print("最终结果:",num) # ==== 执行结果 ==== 三次采集 """ 最终结果: -1472151 最终结果: -2814372 最终结果: -5149396 """
一个加一千万次,一个减一千万次,按理来说最后的结果应该是0,但是为什么相差这么大?更加恐怖的是每次的结果都完全不一样呢?
其实这就是由于线程切换导致出的线程安全问题,因为我们不能精确的知道CPU会在什么时候进行线程的切换,那么如何控制它呢?我们就需要用到锁,其实通俗点来讲你可以这么认为他:
线程安全的目标:
线程之间共同操作一个资源 如何保持资源的同步性
因为线程的切换是由CPU控制的,所以我们要控制它的切换
线程安全的定义:
线程安全:多线程操作时,内部会让所有线程排队处理
线程不安全:我们需要一个机制来让所有线程进行排队处理
那么什么时候我们要考虑线程安全的问题呢?
多个线程对同一数据源进行写入操作时
Ps:在CPython的八大基本数据类型中,
list
和dict
本身就是属于线程安全的容器。
锁的作用
锁就提供给我们能够自行操控线程切换的一种手段,而并非系统自带的切换机制进行切换。
Lock同步锁
方法大全
Lock同步(互斥)锁方法方法大全 | |
---|---|
方法/属性名称 | 功能描述 |
acquire(blocking=True, timeout=-1) | 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。 |
release() | 解锁,解锁后系统可以切换至其他线程运行。 |
locked() | 如果获得了锁则返回真值。 |
使用方式
被上锁和解锁期间的代码块执行时,不会切换至其他线程,有一点要注意的是对于
lock
锁而言,一次acquire()
必须对应一次release()
,不能出现重复的使用两次acquire()
的操作,这会造成死锁!注意:同步锁是一次只能放行一个线程。
import threading num = 0 def add(): lock.acquire() # 上锁 global num for i in range(10000000): # 一千万次 num += 1 lock.release() # 解锁 def sub(): lock.acquire() # 上锁 global num for i in range(10000000): # 一千万次 num -= 1 lock.release() # 解锁 if __name__ == '__main__': lock = threading.Lock() # 实例化同步锁对象 t1 = threading.Thread(target=add, ) t2 = threading.Thread(target=sub, ) t1.start() t2.start() t1.join() t2.join() print("最终结果:", num) # ==== 执行结果 ==== 三次采集 """ 最终结果: 0 最终结果: 0 最终结果: 0 """
import threading num = 0 def add(): lock.acquire() # 上锁 lock.acquire() # 死锁 global num for i in range(10000000): # 一千万次 num += 1 lock.release() lock.release() def sub(): lock.acquire() # 上锁 lock.acquire() # 死锁 global num for i in range(10000000): # 一千万次 num -= 1 lock.release() lock.release() if __name__ == '__main__': lock = threading.Lock() # 实例化同步锁对象 t1 = threading.Thread(target=add,) t2 = threading.Thread(target=sub,) t1.start() t2.start() t1.join() t2.join() print("最终结果:",num) # ==== 执行结果 ==== 三次采集 """ 卡住不动了 """
上下文管理
threading.Lock()
对象中实现了__enter__
与__exit__
方法,因此我们可以使用with
语句进行上下文管理式的加锁。
import threading num = 0 def add(): with lock: # 自动加锁与解锁 global num for i in range(10000000): # 一千万次 num += 1 def sub(): with lock: # 自动加锁与解锁 global num for i in range(10000000): # 一千万次 num -= 1 if __name__ == '__main__': lock = threading.Lock() t1 = threading.Thread(target=add,) t2 = threading.Thread(target=sub,) t1.start() t2.start() t1.join() t2.join() print("最终结果:",num) # ==== 执行结果 ==== 三次采集 """ 最终结果: 0 最终结果: 0 最终结果: 0 """
RLock递归锁
方法大全
Rlock递归锁方法方法大全 | |
---|---|
方法/属性名称 | 功能描述 |
acquire(blocking=True, timeout=-1) | 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。如果已上锁,递归层数增加一层。 |
release() | 解锁,解锁后系统可以切换至其他线程运行。 |
使用方式
RLock
递归锁的使用方式与同步锁相同,唯一的不同点就是可以多次acquire()
,这并不会产生死锁,但是有几次acquire()
就应该有相应的几次release()
,否则依然会造成死锁!注意:递归锁与同步锁相同,也是一次只能放行一个线程。并且也支持上下文管理。
import threading num = 0 def add(): lock.acquire() # 上锁 + 1 lock.acquire() # 上锁 + 1 global num for i in range(10000000): # 一千万次 num += 1 lock.release() # 解锁 - 1 lock.release() # 解锁 - 1 def sub(): lock.acquire() # 上锁 lock.acquire() # 死锁 global num for i in range(10000000): # 一千万次 num -= 1 lock.release() # 解锁 - 1 lock.release() # 解锁 - 1 if __name__ == '__main__': lock = threading.RLock() # 实例化递归锁对象 t1 = threading.Thread(target=add,) t2 = threading.Thread(target=sub,) t1.start() t2.start() t1.join() t2.join() print("最终结果:",num) # ==== 执行结果 ==== 三次采集 """ 最终结果: 0 最终结果: 0 最终结果: 0 """
Condition条件锁
方法大全
Condition条件锁方法方法大全 | |
---|---|
方法/属性名称 | 功能描述 |
acquire(blocking=True, timeout=-1) | 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。 |
release() | 解锁,解锁后系统可以切换至其他线程运行。 |
wait(timeout=None) | 等待唤醒,此时该线程是处于暂停运行状态,要么等待通知要么等待到超时时间后继续执行该线程。 |
wait_for(predicate, timeout=None) | 等待唤醒,直到返回了一个True 。predicate 是一个可调用的对象,其返回值应该是True 或者False 。 |
notify(n=1) | 通知唤醒,可以唤醒多个处于wait() 的线程,默认为1个。 |
notify_all() | 通知唤醒,唤醒所有处于wait() 的线程。 |
使用方式
Condition
条件锁是在递归锁的基础上增加了能够暂停线程运行的功能。并且我们可以使用wait()
与notify()
来控制每个线程执行的个数。注意:条件锁可以自由设定一次放行几个线程。
import threading num = 0 def task(): obj = threading.current_thread() print("当前是线程[{0}],已经开始运行了...".format(obj.getName())) cond.acquire() # 上锁 global num print("当前是线程[{0}],处于等待状态...".format(obj.getName())) cond.wait() # 暂停,等待唤醒 num += 1 print("当前是线程[{0}],等待状态结束,继续运行...".format(obj.getName())) cond.release() # 解锁 if __name__ == '__main__': cond = threading.Condition() # 实例条件锁对象 for i in range(10): t1 = threading.Thread(target=task,) # 开启10条线程 t1.start() # 等待CPU调度执行 while num < 10: task_num = int(input("请输入你要执行的线程数量:")) cond.acquire() cond.notify(task_num) # 通知唤醒 cond.release() print("最终结果:",num) # ==== 执行结果 ==== """ 当前是线程[Thread-1],已经开始运行了... 当前是线程[Thread-1],处于等待状态... 当前是线程[Thread-2],已经开始运行了... 当前是线程[Thread-2],处于等待状态... 当前是线程[Thread-3],已经开始运行了... 当前是线程[Thread-3],处于等待状态... 当前是线程[Thread-4],已经开始运行了... 当前是线程[Thread-4],处于等待状态... 当前是线程[Thread-5],已经开始运行了... 当前是线程[Thread-5],处于等待状态... 当前是线程[Thread-6],已经开始运行了... 当前是线程[Thread-6],处于等待状态... 当前是线程[Thread-7],已经开始运行了... 当前是线程[Thread-7],处于等待状态... 当前是线程[Thread-8],已经开始运行了... 当前是线程[Thread-8],处于等待状态... 当前是线程[Thread-9],已经开始运行了... 当前是线程[Thread-9],处于等待状态... 当前是线程[Thread-10],已经开始运行了... 当前是线程[Thread-10],处于等待状态... 请输入你要执行的线程数量:2 当前是线程[Thread-1],等待状态结束,继续运行... 当前是线程[Thread-2],等待状态结束,继续运行... 请输入你要执行的线程数量:3 当前是线程[Thread-3],等待状态结束,继续运行... 当前是线程[Thread-5],等待状态结束,继续运行... 当前是线程[Thread-4],等待状态结束,继续运行... 请输入你要执行的线程数量:5 当前是线程[Thread-6],等待状态结束,继续运行... 当前是线程[Thread-7],等待状态结束,继续运行... 当前是线程[Thread-8],等待状态结束,继续运行... 当前是线程[Thread-10],等待状态结束,继续运行... 当前是线程[Thread-9],等待状态结束,继续运行... 请输入你要执行的线程数量:1 最终结果: 10 """
上下文管理
import threading num = 0 def task(): obj = threading.current_thread() print("当前是线程[{0}],已经开始运行了...".format(obj.getName())) with cond: global num print("当前是线程[{0}],处于等待状态...".format(obj.getName())) cond.wait() # 暂停,等待唤醒 num += 1 print("当前是线程[{0}],等待状态结束,继续运行...".format(obj.getName())) if __name__ == '__main__': cond = threading.Condition() # 实例化递归锁对象 for i in range(10): t1 = threading.Thread(target=task,) # 开启10条线程 t1.start() # 等待CPU调度执行 while num < 10: task_num = int(input("请输入你要执行的线程数量:")) with cond: cond.notify(task_num) # 通知唤醒 print("最终结果:",num) # ==== 执行结果 ==== """ 当前是线程[Thread-1],已经开始运行了... 当前是线程[Thread-1],处于等待状态... 当前是线程[Thread-2],已经开始运行了... 当前是线程[Thread-2],处于等待状态... 当前是线程[Thread-3],已经开始运行了... 当前是线程[Thread-3],处于等待状态... 当前是线程[Thread-4],已经开始运行了... 当前是线程[Thread-4],处于等待状态... 当前是线程[Thread-5],已经开始运行了... 当前是线程[Thread-5],处于等待状态... 当前是线程[Thread-6],已经开始运行了... 当前是线程[Thread-6],处于等待状态... 当前是线程[Thread-7],已经开始运行了... 当前是线程[Thread-7],处于等待状态... 当前是线程[Thread-8],已经开始运行了... 当前是线程[Thread-8],处于等待状态... 当前是线程[Thread-9],已经开始运行了... 当前是线程[Thread-9],处于等待状态... 当前是线程[Thread-10],已经开始运行了... 当前是线程[Thread-10],处于等待状态... 请输入你要执行的线程数量:2 当前是线程[Thread-1],等待状态结束,继续运行... 当前是线程[Thread-2],等待状态结束,继续运行... 请输入你要执行的线程数量:3 当前是线程[Thread-3],等待状态结束,继续运行... 当前是线程[Thread-5],等待状态结束,继续运行... 当前是线程[Thread-4],等待状态结束,继续运行... 请输入你要执行的线程数量:5 当前是线程[Thread-6],等待状态结束,继续运行... 当前是线程[Thread-7],等待状态结束,继续运行... 当前是线程[Thread-8],等待状态结束,继续运行... 当前是线程[Thread-10],等待状态结束,继续运行... 当前是线程[Thread-9],等待状态结束,继续运行... 请输入你要执行的线程数量:1 最终结果: 10 """
Event事件锁
方法大全
Event事件锁方法方法大全 | |
---|---|
方法/属性名称 | 功能描述 |
is_set() | 用来判断当前红绿灯(标志位)的状态,红灯为False ,绿灯为True 。 |
set() | 通知所有处于红灯状态的线程开始运行,这相当于将标志位改为True 。 |
clear() | 将所有处于绿灯状态的线程暂停,这相当于将标志位改为False 。 |
wait(timeout=None) | 阻塞当前线程直到被放行,即等待红绿灯的通知,红灯停绿灯行。 |
使用方式
这玩意儿是基于
Condition
条件锁做的,跟条件锁的区别就在于他是非常干脆利落的。注意:事件锁只能一次全部放行,相当于红绿灯一样,等车的红灯全停,绿灯全过。
此外,事件锁不支持上下文管理协议。
import threading def task(): obj = threading.current_thread() print("当前是线程[{0}],已经开始运行了...".format(obj.getName())) print("当前是线程[{0}],红灯了,停车...".format(obj.getName())) event.wait() # 暂停,等待绿灯通行 print("当前是线程[{0}],绿灯了放行...".format(obj.getName())) print("当前是线程[{0}],卧槽怎么又是红灯,停车...".format(obj.getName())) event.wait() # 暂停,等待绿灯通行 print("当前是线程[{0}],继续运行...".format(obj.getName())) if __name__ == '__main__': event = threading.Event() # 实例化事件锁对象 for i in range(10): t1 = threading.Thread(target=task,) # 开启10条线程 t1.start() # 等待CPU调度执行 event.set() # 设置为绿灯 针对第一次 event.clear() # 设置为红灯,如果没有他那么上面不管wait()多少次都没用了。因为全都是绿灯 event.set() # 再次设置为绿灯,针对第二次 # ==== 执行结果 ==== """ 当前是线程[Thread-1],已经开始运行了... 当前是线程[Thread-1],红灯了,停车... 当前是线程[Thread-2],已经开始运行了... 当前是线程[Thread-2],红灯了,停车... 当前是线程[Thread-3],已经开始运行了... 当前是线程[Thread-3],红灯了,停车... 当前是线程[Thread-4],已经开始运行了... 当前是线程[Thread-4],红灯了,停车... 当前是线程[Thread-5],已经开始运行了... 当前是线程[Thread-5],红灯了,停车... 当前是线程[Thread-6],已经开始运行了... 当前是线程[Thread-6],红灯了,停车... 当前是线程[Thread-7],已经开始运行了... 当前是线程[Thread-7],红灯了,停车... 当前是线程[Thread-8],已经开始运行了... 当前是线程[Thread-8],红灯了,停车... 当前是线程[Thread-9],已经开始运行了... 当前是线程[Thread-9],红灯了,停车... 当前是线程[Thread-10],已经开始运行了... 当前是线程[Thread-10],红灯了,停车... 当前是线程[Thread-10],绿灯了放行... 当前是线程[Thread-10],卧槽怎么又是红灯,停车... 当前是线程[Thread-5],绿灯了放行... 当前是线程[Thread-9],绿灯了放行... 当前是线程[Thread-9],卧槽怎么又是红灯,停车... 当前是线程[Thread-1],绿灯了放行... 当前是线程[Thread-1],卧槽怎么又是红灯,停车... 当前是线程[Thread-6],绿灯了放行... 当前是线程[Thread-6],卧槽怎么又是红灯,停车... 当前是线程[Thread-3],绿灯了放行... 当前是线程[Thread-4],绿灯了放行... 当前是线程[Thread-2],绿灯了放行... 当前是线程[Thread-1],继续运行... 当前是线程[Thread-5],卧槽怎么又是红灯,停车... 当前是线程[Thread-3],卧槽怎么又是红灯,停车... 当前是线程[Thread-7],绿灯了放行... 当前是线程[Thread-7],卧槽怎么又是红灯,停车... 当前是线程[Thread-7],继续运行... 当前是线程[Thread-4],卧槽怎么又是红灯,停车... 当前是线程[Thread-3],继续运行... 当前是线程[Thread-10],继续运行... 当前是线程[Thread-5],继续运行... 当前是线程[Thread-8],绿灯了放行... 当前是线程[Thread-6],继续运行... 当前是线程[Thread-4],继续运行... """
Semaphore信号量锁
方法大全
Semaphore信号量锁方法方法大全 | |
---|---|
方法/属性名称 | 功能描述 |
acquire(blocking=True, timeout=-1) | 上锁,在上锁状态中的代码块运行时不允许切换至其他线程运行。 |
release() | 解锁,解锁后系统可以切换至其他线程运行。 |
使用方式
这玩意儿是可以规定一次最多跑多少线程的一个东西。也是基于
Condition
条件锁做的。注意:区分与
Condition
条件锁的区别,Semaphore
信号量锁不能自由规定,只能规定一次,而条件锁可以多次规定。
import threading import time def task(): sema.acquire() time.sleep(1) obj = threading.current_thread() print("当前是线程[{0}],已经开始运行了...".format(obj.getName())) sema.release() if __name__ == '__main__': sema = threading.Semaphore(3) # 实例化信号量锁对象,代表每次都跑3条。 for i in range(10): t1 = threading.Thread(target=task,) # 开启10条线程 t1.start() # 等待CPU调度执行 # ==== 执行结果 ==== """ 当前是线程[Thread-1],已经开始运行了... 当前是线程[Thread-3],已经开始运行了... 当前是线程[Thread-2],已经开始运行了... 当前是线程[Thread-4],已经开始运行了... 当前是线程[Thread-6],已经开始运行了... 当前是线程[Thread-5],已经开始运行了... 当前是线程[Thread-7],已经开始运行了... 当前是线程[Thread-9],已经开始运行了... 当前是线程[Thread-8],已经开始运行了... 当前是线程[Thread-10],已经开始运行了... """
上下文管理
import threading import time def task(): with sema: time.sleep(1) obj = threading.current_thread() print("当前是线程[{0}],已经开始运行了...".format(obj.getName())) if __name__ == '__main__': sema = threading.Semaphore(3) # 实例化信号量锁对象,代表每次都跑3条。 for i in range(10): t1 = threading.Thread(target=task,) # 开启10条线程 t1.start() # 等待CPU调度执行 # ==== 执行结果 ==== """ 当前是线程[Thread-1],已经开始运行了... 当前是线程[Thread-3],已经开始运行了... 当前是线程[Thread-2],已经开始运行了... 当前是线程[Thread-4],已经开始运行了... 当前是线程[Thread-6],已经开始运行了... 当前是线程[Thread-5],已经开始运行了... 当前是线程[Thread-7],已经开始运行了... 当前是线程[Thread-9],已经开始运行了... 当前是线程[Thread-8],已经开始运行了... 当前是线程[Thread-10],已经开始运行了... """
扩展:练习题
Condition条件锁的应用
需求:一个空列表,两个线程轮番往里面加值(一个加偶数,一个加奇数),让该列表中的值为 1 - 100。
import threading import time li = [] def even(): """加偶数""" with cond: # 加锁 for i in range(2, 101, 2): if len(li) % 2 != 0: li.append(i) cond.notify() # notify()并不会立即终止当前线程的执行,而是告诉另一线程。你可以走了,不过得等我wait()之后 cond.wait() # 阻塞住,执行另一线程,直到另一线程发送了notify()并且它wait()了之后。 else: cond.wait() li.append(i) cond.notify() else: cond.notify() def odd(): """加奇数""" with cond: for i in range(1, 101, 2): if len(li) %2 == 0: li.append(i) cond.notify() cond.wait() else: cond.notify() if __name__ == '__main__': cond = threading.Condition() t1 = threading.Thread(target=odd) t2 = threading.Thread(target=even) t1.start() t2.start() t1.join() t2.join() print(li)
Event事件锁的应用
有两个线程,如何让他们一人一句对答?文本如下:
杜甫:老李啊,来喝酒!
李白:老杜啊,不喝了我喝不下了!
杜甫:老李啊,再来一壶?
杜甫:...老李?
李白:呼呼呼...睡着了..
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()
扩展:锁的关系浅析
这里我们来聊一聊锁的关系。
Rlock
递归锁,Condition
条件锁,Event
事件锁以及Semaphore
信号量锁内部都是以同步锁为基础的。
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()
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
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