

在之前的文章Python基础篇 - 多线程-线程的创建中,我们认识了多线程的创建;在Python基础篇 - 多线程-线程池中,我们学习了线程池的创建。
我们知道多线程最大的特点就是:共享全局变量数据,而这个特点会导致线程的安全问题,会导致数据错乱、数据丢失、计算结果错误。想要解决线程争抢资源的问题,就得了解互斥锁。

一.多线程-数据安全问题
多线程会争抢资源,这会导致数据结果的错乱,如下面的代码演示:理论上计算,3个线程共累加15次,那这个程序执行完成后的结果是g_num=15,但是事实并非如此。
这是因为在修改 g_num 前,有0.1秒的模拟时长,某个线程延时后,CPU立即分配计算资源给其他线程。此时0.1秒的休眠还未结束,这个线程还未将修改后的数据赋值给 g_num,因此其他线程获取到的并不是最新值,所以才出现上面的结果。
import timeimport threading# 定义任务函数def func():global g_num # 修改全局变量for _ in range(5):t = g_num + 1time.sleep(0.1) # 模拟任务执行时长g_num = tprint(f"{threading.current_thread().name}: {g_num}\n", end="")if __name__ == "__main__":g_num = 0 # 全局变量# 创建多线程-3个线程threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)]# 多线程执行任务[t.start() for t in threads][t.join() for t in threads]print(g_num)
二. 互斥锁
为了解决这个问题,当某个线程要更改共享数据时,先将其锁定,此时其他线程不能更改。直到该线程释放资源,其他的线程才能再次锁定该资源。互斥锁保证了同一时间只有一个线程进行操作,防止多个线程争抢资源,从而保证了多线程情况下数据的正确性。
固定用法:
1.lock = threading.Lock(),创建锁
2.lock.acquire() ,加锁(占用资源)
3.lock.release() ,解锁(释放资源)
import timeimport threading# 任务函数def func():global g_numfor _ in range(5):lock.acquire() # 获取锁t = g_num + 1time.sleep(0.1)g_num = tlock.release() # 释放锁print(f"{threading.current_thread().name}: {g_num}\n", end="")if __name__ == "__main__":g_num = 0 # 全局变量lock = threading.Lock() # 创建锁threads = [threading.Thread(target=func, name=f"线程{i}") for i in range(3)][t.start() for t in threads][t.join() for t in threads]print(g_num) # 15
这个程序执行完成之后,得到的是最终期望结果g_num=15。但是需要特别注意两个点。
1. 上锁了,但是忘记了解锁:这会造成线程永久阻塞、程序卡死(死锁)。
2. 锁加在循环外面的情况:所有代码一次性锁住,多线程直接变单线程,失去并发意义。当锁加循环外面,那只能等一个线程执行5遍任务,第二个线程才能进来执行任务,以此类推就是单线程任务。但是,锁加在循环里面,在一个线程执行任务耗时结束后,第二个线程就可以参与任务,不会导致一个线程循环跑5次的结果。

总结:今日我们学习了Python基础篇-多线程-互斥锁。由于多线程共享全局变量,多个线程同时修改数据会产生资源竞争,导致数据错乱丢失。我们使用threading.Lock创建互斥锁,通过上锁、解锁操作,保证同一时刻仅有一条线程修改共享数据,解决线程安全问题。加锁后程序由并行变为串行,牺牲运行速度换取数据准确性。在Python基础篇,后续我们将持续学习Python基础知识,介绍各种算法,祝每一位study er学习愉快。

