Python多线程共享全局变量的竞态问题与解决方案

咖啡半糖
咖啡半糖
发布时间:2026-10-13| 6人浏览过
原创

本文探讨Python多线程共享全局变量时因执行顺序不确定导致的竞态条件问题。通过示例展示直接累加可能产生错误结果,并对比轮询方式与通知方式(如全局标志位)的效率差异。最终推荐使用Lock锁机制确保数据一致性,保证多线程环境下全局变量更新的原子性与正确性。

通常情况下,在多线程编程中,多个线程会共享同一块内存资源。

from threading import Thread
 
global_num = 0
 
def func1():
 global global_num
 for i in range(1000000):
 global_num += 1
 print('---------func1:global_num=%s--------'%global_num)
 
def func2():
 global global_num
 for i in range(1000000):
 global_num += 1
 print('--------fun2:global_num=%s'%global_num)
print('global_num=%s'%global_num)
 
lock = Lock()
 
t1 = Thread(target=func1)
t1.start()
 
t2 = Thread(target=func2)
t2.start()

运行上述代码后,观察到的输出结果如下:

global_num=0
---------func1:global_num=1492752--------
--------fun2:global_num=1515462

#与多进程不同,每个进程拥有独立的资源块,多线程共享主线程的资源块(尽管这种表述在严格意义上并不完全准确)。

#这种共享机制虽然简化了线程间的数据传递,但由于线程执行的调度顺序具有不确定性,最终输出的结果往往并非预期的正确值。

#以下示例中,若未设置global_flag全局变量作为同步标志,逻辑上预期最终结果应为2000000(选择如此大的数值是为了更显著地暴露问题)。

#然而实际运行结果通常小于2000000。虽然理论上存在偶然得到2000000的理想情况,但这属于极小概率事件。造成这种现象的原因何在?

#根本原因在于CPU对线程调度的顺序是不确定的。具体而言,主线程创建了两个子线程t1和t2,分别指向func1()和func2()函数。

#在这两个线程的函数体中,均包含“global_num += 1”这一行代码。在计算机底层执行时,该语句实际分为两个步骤:第一步是从内存读取global_num的当前值并完成加一运算,此时global_num的值仍为旧值;第二步是将计算后的新值写回global_num,此时global_num的值才真正更新。在程序执行过程中,可能出现以下情况:

#当CPU执行线程t1中的累加语句时,在执行完第一步(读取并计算)后暂停,将线程t1挂起,转而执行线程t2。当线程t2执行一段时间后,也发生类似情况,

#随后CPU重新调度执行线程t1,此时正好执行累加语句的第二步(赋值)。由于t1和t2读取的是相同的旧值,t1的赋值覆盖了t2未完成的计算,导致这段时间内的累加操作失效,因此最终结果往往不等于2000000。

#解决此类问题可以采用引入额外变量并利用“轮询”方式执行,但这种方式效率低下且浪费CPU资源,因此通常采用“通知”机制来处理。

轮询方式实现如下:

from threading import Thread
 
global_num = 0
global_flag = 0
 
def func1():
	global global_num
	global global_flag
	if global_flag == 0:
		for i in range(1000000):
			global_num += 1
	global_flag = 1	
	print('---------func1:global_num=%s--------'%global_num)
 
def func2():
	global global_num
	while True:
		if global_flag != 0:
			for i in range(1000000):
				global_num += 1
			break
	print('--------fun2:global_num=%s'%global_num)
 
print('global_num=%s'%global_num)
 
t1 = Thread(target=func1)
t1.start()
 
t2 = Thread(target=func2)
t2.start()

运行结果:

global_num=0
---------func1:global_num=1000000--------
--------fun2:global_num=2000000

通知方式实现如下:

from threading import Thread,Lock
 
 
global_num = 0
 
def func1():
	global global_num
	for i in range(1000000):
		lock.acquire()#两个线程会最开始抢这个锁,拿到锁就会处于关锁,执行后面的程序,其他线程执行处于监听状态,等待这个线程开锁,再抢锁
		global_num += 1
		lock.release()
	print('---------func1:global_num=%s--------'%global_num)
 
def func2():
	global global_num
	for i in range(1000000):
		lock.acquire()
		global_num += 1
		lock.release()
	print('--------fun2:global_num=%s'%global_num)
print('global_num=%s'%global_num)
 
lock = Lock()
 
t1 = Thread(target=func1)
t1.start()
 
t2 = Thread(target=func2)
t2.start()

输出结果:

global_num=0
---------func1:global_num=1901175--------
--------fun2:global_num=2000000

以上关于Python多线程处理同一全局变量的进阶方法即为本文全部内容,希望能为读者提供参考,也欢迎大家继续支持本站。

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。