文章目录
一、前言
python多线程与单片机的RTOS在调度规则完全不一样。python多线程的调度程序会在任何时候中断线程(相当于调度,所以python多线程的调度可以说是很难控制的),单片机的RTOS会有相应的API来产生调度(调度是可控的)。
线程与协程之间的比较还有最后一点要说明:如果使用线程做过重要的编程,就知道写出程序有多么困难,因为调度程序任何时候都可能中断线程。必须记住保留锁,去保护程序中的重要部分,防止多步操作在执行的过程中被中断,防止数据处于无效状态。 —《流畅的Python》- Luciano Ramalho
所以,在我看来:python的协程与单片机的RTOS才是相似的。
—加粗样式
多线程的优势在于并发性,即可以同时运行多个任务。但是当线程需要使用共享数据时,也可能会由于数据不同步产生“错误情况”,这是由系统的线程调度具有一定的随机性造成的。
由于线程之间的任务执行是CPU进行随机调度的,并且每个线程可能只执行了n条指令之后就被切换到别的线程了。当多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,这被称为“线程不安全”。为了保证数据安全,设计了线程锁,即同一时刻只允许一个线程操作该数据。
B站有一个视频讲解线程安全问题,个人觉得不错。
【2021最新版】Python 并发编程实战,用多线程、多进程、多协程加速程序运行
二、线程不安全的现象
2.1、代码
代码的目的是让number累加到2000000(2百万),线程counter_1将number累加到1000000,线程counter_2将number累加到2000000。
# python3.9
import threading
import time
number = 0
# lock = threading.Lock()
def counter_1():
"""
子线程1,counter_1
"""
global number # 声明number是全局变量,并不是函数的局部变量
# global lock
# lock.acquire()
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
# lock.release()
def counter_2():
"""
子线程2,counter_2
"""
global number # 声明number是全局变量,并不是函数的局部变量
# global lock
# lock.acquire()
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
# lock.release()
def main():
t1 = threading.Thread(target=counter_1,name="counter_1",daemon=True)
t2 = threading.Thread(target=counter_2,name="counter_2",daemon=True)
t1.start()
t2.start()
t1.join()
t2.join()
print("程序运行结束,number =",number)
if __name__ == "__main__":
main()
2.2、运行
从运行的四次结果看来,每一次的结果都不一样,而且都没有出现一次2000000。这个现象就是因为多个线程同时访问一个对象所造成的线程不安全问题。
三、使用互斥锁解决线程不安全
3.1、代码
# python3.9
import threading
import time
number = 0
lock = threading.Lock() # 互斥锁
def counter_1():
"""
子线程1,counter_1
"""
global number # 声明number是全局变量,并不是函数的局部变量
global lock # 声明lock是全局变量,并不是函数的局部变量
lock.acquire() # 获取互斥锁
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
lock.release() # 释放互斥锁(千万记得用完要释放,否则会出现死锁)
def counter_2():
"""
子线程2,counter_2
"""
global number # 声明number是全局变量,并不是函数的局部变量
global lock # 声明lock是全局变量,并不是函数的局部变量
lock.acquire() # 获取互斥锁
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
lock.release() # 释放互斥锁(千万记得用完要释放,否则会出现死锁)
def main():
t1 = threading.Thread(target=counter_1,name="counter_1",daemon=True) #创建线程counter_1
t2 = threading.Thread(target=counter_2,name="counter_2",daemon=True) #创建线程counter_2
t1.start() # 启动线程counter_1
t2.start() # 启动线程counter_2
t1.join() # 阻塞主线程,等待线程counter_1运行结束
t2.join() # 阻塞主线程,等待线程counter_2运行结束
print("程序运行结束,number =",number)
if __name__ == "__main__":
main()
3.2、运行
四次运行的结果都符合预期的2000000,表示互斥锁解决了线程不安全的问题。但是,获取互斥锁并使用完之后一定,一定,一定要释放互斥锁。否则,会出现死锁的问题。(其他获取互斥锁的线程将一直阻塞在那里。)
四、忘记释放互斥锁,造成死锁
4.1、代码
# python3.9
import threading
import time
number = 0
lock = threading.Lock() # 互斥锁
def counter_1():
"""
子线程1,counter_1
"""
global number # 声明number是全局变量,并不是函数的局部变量
global lock # 声明lock是全局变量,并不是函数的局部变量
lock.acquire() # 获取互斥锁
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
# lock.release() # 释放互斥锁(千万记得用完要释放,否则会出现死锁)
def counter_2():
"""
子线程2,counter_2
"""
global number # 声明number是全局变量,并不是函数的局部变量
global lock # 声明lock是全局变量,并不是函数的局部变量
lock.acquire() # 获取互斥锁
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
lock.release() # 释放互斥锁(千万记得用完要释放,否则会出现死锁)
def main():
t1 = threading.Thread(target=counter_1,name="counter_1",daemon=True) #创建线程counter_1
t2 = threading.Thread(target=counter_2,name="counter_2",daemon=True) #创建线程counter_2
t1.start() # 启动线程counter_1
t2.start() # 启动线程counter_2
t1.join() # 阻塞主线程,等待线程counter_1运行结束
t2.join() # 阻塞主线程,等待线程counter_2运行结束
print("程序运行结束,number =",number)
if __name__ == "__main__":
main()
在这里我故意忘记将获取的互斥锁释放回去了,会出现什么问题?
4.2、运行
程序卡死在哪里??我在代码上加入一句代码看看程序是不是真的卡在线程counter_2里。
运行代码看看。
运行的结果证明了线程counter_2被阻塞在lock.acquire( )代码这里,并一直一直阻塞下去,造成整个python程序卡死。
所以,紧记要释放互斥锁。
4.3、造成死锁的一种常见案例
为了简化问题,我们设有两个并发的线程( 线程A 和 线程B ),需要 资源1 和 资源2 .假设 线程A 需要 资源1 , 线程B 需要 资源2 .在这种情况下,两个线程都使用各自的锁,目前为止没有冲突。现在假设,在双方释放锁之前, 线程A 需要 资源2 的锁, 线程B 需要 资源1 的锁,没有资源线程不会继续执行。鉴于目前两个资源的锁都是被占用的,而且在对方的锁释放之前都处于等待且不释放锁的状态。这是死锁的典型情况。所以如上所说,使用锁来解决同步问题是一个可行却存在潜在问题的方案。 —摘自《python并行编程中文版》
五、with语句拯救粗心的人类
5.1、with语句在互斥锁上的使用
# python3.9
import threading
import time
number = 0
lock = threading.Lock() # 互斥锁
def counter_1():
"""
子线程1,counter_1
"""
global number # 声明number是全局变量,并不是函数的局部变量
global lock # 声明lock是全局变量,并不是函数的局部变量
# 使用with语句管理互斥锁
with lock:
# 锁的对象
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
def counter_2():
"""
子线程2,counter_2
"""
global number # 声明number是全局变量,并不是函数的局部变量
global lock # 声明lock是全局变量,并不是函数的局部变量
# 使用with语句管理互斥锁
with lock:
# 锁的对象
for i in range(1000000):
number +=1
print("子线程%s运算结束后,number = %s" % (threading.current_thread().getName(),number))
def main():
t1 = threading.Thread(target=counter_1,name="counter_1",daemon=True) #创建线程counter_1
t2 = threading.Thread(target=counter_2,name="counter_2",daemon=True) #创建线程counter_2
t1.start() # 启动线程counter_1
t2.start() # 启动线程counter_2
t1.join() # 阻塞主线程,等待线程counter_1运行结束
t2.join() # 阻塞主线程,等待线程counter_2运行结束
print("程序运行结束,number =",number)
if __name__ == "__main__":
main()
5.3、运行的结果
从运行的结果看来并没有出现线程不安全问题。
with语句真伟大又简洁!!!!