Python多线程共享全局变量:使用互斥锁解决数据竞争

洛溪
洛溪
发布时间:2026-10-14 | 5人浏览过
原创

本文深入解析Python多线程编程中共享全局变量引发的数据竞争问题。通过引入互斥锁(Mutex)机制,确保多个线程安全访问共享资源。文章详细阐述了同步概念、互斥锁的工作原理及acquire/release方法的使用,并提供了完整的代码示例,展示如何正确使用上锁与解锁来保证数据一致性,同时强调了最小化锁范围以优化性能的最佳实践。

同步概念与线程协作

一、同步概念

同步指的是协同步调,按照预定的先后顺序执行操作。例如:一方说完,另一方再说。

从字面理解,“同”似乎意味着一起行动。

但实际上,这里的“同”应理解为协同、协助以及互相配合。

线程同步可以理解为线程A和B协同工作:当线程A执行到一定程度需要依赖线程B的某个结果时,它会暂停并示意线程B运行;线程B执行完毕后,将结果返回给线程A,随后线程A继续操作。

此前我们曾遇到多个线程共同修改某个数据时可能出现不可预知结果的情况。为了保证数据的正确性,必须对多个线程进行同步处理。

解决全局变量竞争问题

解决线程同时修改全局变量的方式

我们回顾一下之前提到的那个问题。


import threading
import time

g_num = 0

def work1(num):
 global g_num
 for i in range(num):
  g_num += 1
 print("----in work1, g_num is %d---" % g_num)

def work2(num):
 global g_num
 for i in range(num):
  g_num += 1
 print("----in work2, g_num is %d---" % g_num)

print("---线程创建之前g_num is %d---" % g_num)

t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()

t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()

# 确保子线程都运行结束
while len(threading.enumerate()) != 1:
 time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

---线程创建之前g_num is 0--- ----in work2, g_num is 1048576--- ----in work1, g_num is 1155200--- 2个线程对同一个全局变量操作之后的最终结果是:1155200

针对上述计算错误的问题,可以通过线程同步机制来解决。

解决思路如下:

系统调用线程t1,此时获取到g_num的值为0,随即进行上锁操作,即禁止其他线程访问g_num。

接着,t1对g_num的值执行+1操作。

随后t1解锁,此时g_num的值变为1,其他线程方可使用g_num,且其值已更新为1而非初始的0。

同理,其他线程在修改g_num时,均需先上锁,处理完毕后再解锁。在上锁的整个过程中,其他线程无法访问该资源,从而确保了数据的正确性。

基本思路如此,那么具体如何通过代码实现呢?

互斥锁机制与代码实现

二、互斥锁解决资源竞争的问题

当多个线程几乎同时修改某一共享数据时,必须进行同步控制。

线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制便是引入互斥锁。

互斥锁为资源引入了一种状态:锁定或非锁定。

当某个线程要更改共享数据时,先将其锁定,此时资源状态为“锁定”,其他线程无法更改;直到该线程释放资源,将资源状态变为“非锁定”,其他线程才能再次锁定该资源。

互斥锁保证了每次只有一个线程进行写入操作,从而确保了多线程环境下数据的正确性。

threading模块中定义了Lock类,可以方便地处理锁定:


# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

注意:

如果该锁之前未上锁,那么acquire调用不会阻塞。

如果在调用acquire对该锁上锁之前,它已被其他线程锁定,那么此时acquire会阻塞,直到该锁被解锁为止。

示例:

使用互斥锁完成2个线程对同一个全局变量各加100万次的操作。


import threading
import time

g_num = 0

def test1(num):
 global g_num
 for i in range(num):
  mutex.acquire() # 上锁
  g_num += 1
  mutex.release() # 解锁

 print("---test1---g_num=%d" % g_num)

def test2(num):
 global g_num
 for i in range(num):
  mutex.acquire() # 上锁
  g_num += 1
  mutex.release() # 解锁

 print("---test2---g_num=%d" % g_num)

# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()

# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()

p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()

# 等待计算完成
while len(threading.enumerate()) != 1:
 time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

---test1---g_num=1989108 ---test2---g_num=2000000 2个线程对同一个全局变量操作之后的最终结果是:2000000

可以看到最后的结果,加入互斥锁后,其结果与预期相符。

记住,上锁的代码范围要越小越好。在业务逻辑正确的前提下,能锁一行代码,就不要锁两行。

锁的状态转换与总结

上锁解锁过程

当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

每次只有一个线程可以获得锁。

如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

总结

锁的好处:

确保了某段关键代码只能由一个线程从头到尾完整地执行。

锁的坏处:

阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。

由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。

到此这篇关于互斥锁解决 Python 中多线程共享全局变量的问题的文章就介绍到这了,更多相关Python 多线程共享全局变量内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表