global 关键字回顾(单线程)
我们简单回顾一下 global 在单线程中的作用。

在 Python 中,当你在一个函数内部读取一个变量时,它会默认在局部作用域查找,如果没有找到,就去全局作用域查找,当你赋值(修改)一个变量时,Python 会认为你正在创建一个新的局部变量,除非你明确声明这个变量是全局的。
示例:
# 全局变量
global_var = 10
def modify_without_global():
# 尝试修改全局变量,但没有声明 global
# 这行代码实际上会创建一个新的局部变量 global_var
# 它不会影响外面的全局变量
global_var = 20
print(f"函数内部 (无 global): {global_var}")
def modify_with_global():
# 明确声明 global_var 是全局变量
global global_var
global_var = 30
print(f"函数内部 (有 global): {global_var}")
# --- 测试 ---
print(f"初始全局变量: {global_var}")
modify_without_global()
print(f"调用 modify_without_global 后: {global_var}") # 全局变量没有被修改
print("-" * 20)
print(f"初始全局变量: {global_var}")
modify_with_global()
print(f"调用 modify_with_global 后: {global_var}") # 全局变量被成功修改
输出:
初始全局变量: 10
函数内部 (无 global): 20
调用 modify_without_global 后: 10
--------------------
初始全局变量: 10
函数内部 (有 global): 30
调用 modify_with_global 后: 30
global 在多线程中的核心问题:共享状态
当多个线程同时访问和修改一个全局变量时,就会引入并发问题,Python 的 global 声明本身并不能解决这些问题,它只是让线程能够访问到同一个内存地址。

主要问题有两个:
- 竞争条件:多个线程同时读取和修改同一个变量,最终结果取决于线程的调度顺序,是不可预测的。
- 原子性操作缺失:像
x = x + 1这样的操作,在底层不是一步完成的,它包含了“读取-修改-写入”三个步骤,如果在执行过程中,另一个线程也介入,就会导致数据不一致。
示例:竞争条件
让我们看一个经典的例子,多个线程同时增加一个全局计数器。
import threading
# 全局变量
counter = 0
def increment():
"""增加全局计数器"""
global counter
for _ in range(100000):
# 1. 读取 counter
# 2. counter + 1
# 3. 将结果写回 counter
# 在这三步之间,可能发生线程切换
counter += 1
# 创建并启动线程
threads = []
for _ in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print(f"最终的计数器值: {counter}")
预期结果: 10 * 100000 = 1,000,000
实际结果: 几乎总是小于 1,000,000,并且每次运行都可能不同。

为什么?
假设 counter 当前是 100。
- 线程 A 读取
counter(100),然后被操作系统暂停。 - 线程 B 读取
counter(100),执行+1,写回101。 - 线程 A 恢复执行,它已经读取了
100,执行+1,写回101。 - 结果是两次操作,但
counter只增加了 1,而不是 2,这就是一次“丢失的更新”。
解决方案:使用锁
为了解决上述竞争条件问题,我们需要确保在某个线程修改共享资源时,其他线程不能同时访问,这个机制就是锁。
在 Python 中,我们可以使用 threading.Lock。
lock.acquire(): 获取锁,如果锁已经被其他线程获取,则当前线程会阻塞,直到锁被释放。lock.release(): 释放锁。with lock:: 这是更推荐、更安全的用法,它会自动处理锁的获取和释放,即使在代码块中发生异常也能保证锁被释放。
使用锁修复上面的例子
import threading
# 全局变量
counter = 0
# 创建一个锁对象
lock = threading.Lock()
def increment_with_lock():
"""使用锁来安全地增加全局计数器"""
global counter
for _ in range(100000):
with lock: # 在 with 代码块内,同一时间只有一个线程能进入
counter += 1
# 创建并启动线程
threads = []
for _ in range(10):
t = threading.Thread(target=increment_with_lock)
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print(f"使用锁后的最终计数器值: {counter}")
输出:
使用锁后的最终计数器值: 1000000
这次结果总是正确的,因为锁确保了 counter += 1 这个操作的原子性。
global 多线程最佳实践
-
最小化共享状态:最好的方法是避免使用全局变量,尽量通过参数传递数据给线程函数,或者使用面向对象的方式,将共享数据封装在类的实例变量中,并通过实例方法来访问。
-
始终保护共享数据:如果一个全局变量(或任何共享数据)会被多个线程写入,那么你必须使用锁来保护它,如果只是读取,并且数据在创建后不会被修改(即“只读共享”),则通常不需要锁。
-
优先使用
with lock::这比手动调用acquire()和release()更安全,因为它能防止忘记释放锁(死锁)。 -
注意死锁:如果线程 A 锁住了对象 1 并等待对象 2,而线程 B 锁住了对象 2 并等待对象 1,两者将永远等待下去,要避免复杂的锁依赖关系。
global vs. 类实例变量
在多线程中,将数据封装在类中通常是更好的设计模式,让我们看看如何用类来实现计数器。
import threading
class SharedCounter:
def __init__(self):
self.value = 0
self.lock = threading.Lock()
def increment(self):
"""安全地增加计数器"""
with self.lock:
self.value += 1
# 创建一个共享的计数器实例
shared_counter = SharedCounter()
def worker():
"""工作线程函数"""
for _ in range(100000):
shared_counter.increment()
# 创建并启动线程
threads = []
for _ in range(10):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print(f"类实例变量方式的最终计数器值: {shared_counter.value}")
这个版本和 global + Lock 的版本功能相同,但结构更清晰,数据和方法被封装在一起,减少了全局状态带来的混乱,更易于维护和扩展。
| 特性 | global 关键字 |
threading.Lock |
|---|---|---|
| 作用 | 声明一个变量是全局作用域的,允许函数修改它。 | 提供一种同步机制,确保对共享资源的独占访问。 |
| 解决的问题 | 函数内部无法直接修改全局变量(会创建局部变量)。 | 竞争条件、数据不一致。 |
| 关系 | global 只是让线程能访问到同一个内存地址,它不提供任何保护。 |
Lock 必须与 global 或共享实例变量结合使用,来保护对共享数据的修改。 |
| 最佳实践 | 尽量避免使用全局变量,如果必须使用,请务必用 Lock 保护。 |
在任何可能发生竞争条件的共享数据修改操作周围使用锁。 |
核心思想:
global是“通行证”:它告诉函数“你要修改的这个变量,去全局作用域找,别在本地创建新的”。Lock是“单间房”:它确保在某个线程修改共享数据时,其他线程必须在外面排队等待,直到修改完成。
在多线程编程中,你需要 global 来让线程看到共享数据,然后用 Lock 来保证数据被正确地修改。
