Python 线程同步
- Python 线程同步
Python 线程同步
线程同步是一种确保两个或多个线程不同时执行同一块共享代码的机制。共享块中的代码通常是访问共享数据或资源,这种共享块被称作临界区。这个概念可以用下面的图清晰地表示出来:
多个线程在同一时间记问临界区,就会有可能同时尝试访问或改变数据,这有可能会导致不可预知的后果。这种情况被称作竞态条件。
为了讲解什么是竞态条件,我们将实现一个包含两个线程的简单的程序,每个线程都会给一个共享变量增加值一百万次。我们选择一个较大的数,是为了确保发现竞态条件。在速度比较低的 CPU 上,用一个小一点的数,也有可能发现竞态条件。在这个程序中,我们使用同一个函数(inc
)创建两个线程。访问共享变量并且每次给它加 1 的代码在临界区里,两个线程访问这个变量时,没有做任何保护。下面是完整的例子:
from threading import Thread, current_thread
from time import sleepdef inc():global xfor _ in range(1000000):x += 1# 全局变量
x = 0# 创建线程
t1 = Thread(target=inc, name="线程 1")
t2 = Thread(target=inc, name="线程 2")# 启动线程
t1.start()
t2.start()# 等待线程结束
t1.join()
t2.join()print("最后 x 的值是:", x)
在上面的这个程序中,我们期望 x
最后的值是 2000000
,但是在程序的输出结果里,我们可能看不到 2000000
。每次执行这个程序,都有可能看到不一样的结果,这个结果会比 2000000
小一些。让我们看一下代码,线程 1
和 线程 2
同时执行临界区代码 x += 1
。两个线程都会请求 x
的当前值。如果当前的值是 1000
,两个线程都会得到 1000
,并且给它加上 1
,得到 1001
。接下来,这两个线程都会把 1001
写回到内存里去。这样最终 x
的值就是 1001
。然而,实际上,每个线程都给 x
增加了 1
,现在 x
应该是 1002
才对。要改正这种错误,就要用到线程的同步机制。
线程的同步可以使用 Lock
类实现,这个类存在于 Thread
模块中。这个锁是用由操作统提供的信号量实现的。信号量是操作系统级别的同步对象,用来控制资源和数据在多处理器程多线程中的访问。Lock
类提供了两个方法,acquire
和 release
,详见如下描述:
-
acquire
方法用来获取锁。锁可以是阻塞式的(默认)或非阻塞式的。如果是一个阻塞锁,调用acquire
的线程会被阻塞,直到另外的拥有锁的线程调用release
方法。一旦拥有锁的线程调用了release
方法,就会有一个调用acquire
的钱程获得锁。如果锁是非阻塞式的,线程在调用acquire
后不会被阻塞,如果锁没有锁,那么线程就会拥有锁;如果锁已经被占有,那么acquire
就会返回False
; -
release
方法用来释放锁,意味着把锁重置为非锁状态。锁被重置为非锁状态后,如果有很多线程在等待获取锁,那么也只有一个线程可以获得锁。
下面的代码演示了在 x
的增加语句前后使用锁:
from threading import Thread, Lockdef inc_with_lock(lock):global xfor _ in range(1000000):lock.acquire()x += 1lock.release()# 全局变量
x = 0
mylock = Lock()# 创建线程
t1 = Thread(target=inc_with_lock, args=(mylock,), name="线程 1")
t2 = Thread(target=inc_with_lock, args=(mylock,), name="线程 2")# 启动线程
t1.start()
t2.start()# 等待线程结束
t1.join()
t2.join()print("最后 x 的值是:", x)
以上代码的输出结果是:
最后 x 的值是: 2000000
使用锁后,x
的值就会一直是 2000000
。·lock
对象确保任一时间,只有一个线程操作 x
。
<完>