线程访问全局变量与线程安全问题
摘要
本篇文章探讨了线程访问全局变量及其可能引发的安全问题。在多线程编程中,全局变量可以方便地在不同线程之间共享数据,但同时也带来了线程非安全的风险。通过示例代码演示了全局变量的访问和修改,并说明了线程非安全可能导致的数据混乱情况。此外,还介绍了线程安全问题,以一个卖票的场景为例,展示了多个线程对共享资源进行操作时可能出现的问题。
技术标签
- 多线程编程
- 全局变量访问
- 线程非安全
- 线程同步
- 共享资源管理
同步
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。同步就是协同步调,按预定的先后次序进行运行。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁
互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便地处理锁定:
# 创建锁 mutex = threading.Lock() # 锁定 mutex.acquire() # 释放 mutex.release()
注意:
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞。
- 如果在调用acquire对这个锁上锁之前,它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止。
和文件操作一样,Lock也可以使用with语句快速地实现打开和关闭操作。
使用互斥锁解决卖票问题
import threading import time ticket = 20 lock = threading.Lock() def sell_ticket(): global ticket while True: lock.acquire() if ticket > 0: time.sleep(0.5) ticket -= 1 lock.release() print('{}卖了一张票,还剩{}'.format(threading.current_thread().name, ticket)) else: print('{}票卖完了'.format(threading.current_thread().name)) lock.release() break for i in range(5): t = threading.Thread(target=sell_ticket, name='thread-{}'.format(i + 1)) t.start()
上锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行。
锁的坏处: - 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。