安全共享数据
使用队列和锁
安全共享数据 是 CoddyKit 上的免费 Python Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Python Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Python Academy 课程共包含 4 节课。
竞态条件问题
当多个线程修改同一份数据时,它们的操作可能交错执行,从而破坏结果。这就是竞态条件。我们需要使用同步工具来安全地共享数据。
import threading
counter = 0
def bump():
global counter
for _ in range(100000):
counter += 1
ts = [threading.Thread(target=bump) for _ in range(2)]
for t in ts: t.start()
for t in ts: t.join()
print('counter (may be < 200000):', counter)Lock 解决问题
threading.Lock 确保同一时刻只有一个线程进入临界区。请在共享数据更新外层使用 with 代码块来获取它。
import threading
counter = 0
lock = threading.Lock()
def bump():
global counter
for _ in range(100000):
with lock:
counter += 1
ts = [threading.Thread(target=bump) for _ in range(2)]
for t in ts: t.start()
for t in ts: t.join()
print('counter:', counter)acquire 和 release
可以手动使用 acquire() 和 release() 操作锁,但 with 形式更安全,因为即使发生错误也会释放锁。
import threading
lock = threading.Lock()
lock.acquire()
try:
print('inside critical section')
finally:
lock.release()
print('released')线程安全的队列
queue.Queue 类本身就是线程安全的。线程可以在没有显式锁的情况下调用 put 和 get,因此队列是传递数据的首选方式。
import queue, threading
q = queue.Queue()
def producer():
for i in range(3):
q.put(i)
t = threading.Thread(target=producer)
t.start()
t.join()
while not q.empty():
print('got', q.get())生产者与消费者
经典模式是:生产者将工作 put 到队列中,消费者 get 出工作并进行处理。队列会处理所有加锁操作。
import queue, threading
q = queue.Queue()
results = []
def consumer():
while True:
item = q.get()
if item is None:
break
results.append(item * 10)
q.task_done()
t = threading.Thread(target=consumer)
t.start()
for i in range(3):
q.put(i)
q.put(None)
t.join()
print(results)join 和 task_done
q.join() 会阻塞,直到每个被 put 的项目都对应调用了 task_done()。这样主线程就可以等待所有工作完成。
import queue, threading
q = queue.Queue()
def worker():
while True:
item = q.get()
q.task_done()
threading.Thread(target=worker, daemon=True).start()
for i in range(5):
q.put(i)
q.join()
print('all items processed')RLock 用于可重入
如果同一个线程尝试获取普通的 Lock 两次,就会发生死锁。threading.RLock(可重入锁)允许这样做,并会记录获取次数。
import threading
rlock = threading.RLock()
def outer():
with rlock:
inner()
def inner():
with rlock:
print('re-entered safely')
outer()Event 用于发出信号
threading.Event 允许一个线程向其他线程发出信号。线程会调用 wait() 等待,直到另一个线程调用 set()。
import threading
start = threading.Event()
def worker():
start.wait()
print('go!')
t = threading.Thread(target=worker)
t.start()
print('setting event')
start.set()
t.join()跨进程共享数据
跨进程共享数据时,请改用 multiprocessing.Queue。它会在独立的内存空间之间序列化对象。
import multiprocessing as mp
def producer(q):
for i in range(3):
q.put(i * i)
if __name__ == '__main__':
q = mp.Queue()
p = mp.Process(target=producer, args=(q,))
p.start()
p.join()
while not q.empty():
print('received', q.get())避免死锁
当线程永远等待彼此持有的锁时,就会发生死锁。请确保在所有地方都按相同顺序获取多个锁,以避免死锁。
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def safe():
with lock_a:
with lock_b:
print('acquired in consistent order')
safe()优先使用队列而不是锁
在可能的情况下,请优先使用队列进行消息传递,而不是使用锁保护共享的可变状态。这样更容易分析,也更不容易产生错误。
import queue
q = queue.Queue()
for word in ['safe', 'simple', 'clear']:
q.put(word)
while not q.empty():
print(q.get())快速检查
请测试您对安全数据共享的理解。
总结
您已学会如何安全地共享数据:
- 未受保护的共享状态会因竞态条件而损坏。
Lock和RLock可以保护临界区。queue.Queue是线程安全的;跨进程时请使用multiprocessing.Queue。Event可以在线程之间发出信号;保持一致的加锁顺序可以避免死锁。
下一课程:使用子进程运行外部程序。
常见问题解答
「安全共享数据」课时是免费的吗?
是的 — 「安全共享数据」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Python Academy 课程的其余内容,请升级到 CoddyKit PRO。 Python Academy 课程共包含 4 节课。
「安全共享数据」这节课中我会学到什么?
使用队列和锁 你通过在浏览器中直接运行的动手代码来练习 Python Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Python Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Python Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「安全共享数据」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Python Academy 课中编写并运行代码吗?
能。每节 Python Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。