Python 中的线程锁

Muhammad Maisam Abbas 2023年1月30日 2022年5月18日
  1. Python 中的竞争条件
  2. Python 中的线程锁
  3. 在 Python 中使用 with lock: 的线程锁
Python 中的线程锁

本教程将讨论在 Python 中使用线程锁的不同方法。

Python 中的竞争条件

竞争条件是当多个线程尝试修改同一个共享变量时出现的问题。所有线程同时从共享变量中读取相同的值。然后,所有线程尝试修改共享变量的值。但是,该变量最终只会存储最后一个线程的值,因为它会覆盖前一个线程写入的值。从这个意义上说,所有线程之间存在竞争,以查看最终哪个线程修改了变量的值。以下代码中的示例演示了这种现象。

from threading import Thread

counter = 0

def increase(by):
    global counter
    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f'counter={counter}')

t1 = Thread(target=increase, args=(10,))
t2 = Thread(target=increase, args=(20,))

t1.start()
t2.start()

t1.join()
t2.join()

print(f'The final counter is {counter}')

输出:

counter=10
counter=20
The final counter is 20

我们有一个全局共享变量 counter = 0 和两个线程 t1t2。线程 t1 尝试将 counter 的值增加 10,线程 t2 尝试将 counter 的值增加 20。在上面的代码中,我们同时运行两个线程并尝试修改值计数器。根据上述逻辑,counter 的最终值应该是 30。但是,由于竞争条件,counter 要么是 10,要么是 20。

Python 中的线程锁

线程锁用于防止竞争条件。线程锁在被一个线程使用时锁定对共享变量的访问,以便任何其他线程无法访问它,然后在线程不使用共享变量时移除锁,以便其他线程可以使用该变量进行处理。threading 模块中的 Lock class 用于在 Python 中创建线程锁。acquire() 方法用于锁定对共享变量的访问,而 release() 方法用于解锁锁定。如果在未锁定的锁上使用 release() 方法会引发 RuntimeError 异常。

from threading import Thread, Lock

counter = 0

def increase(by, lock):
    global counter

    lock.acquire()

    local_counter = counter
    local_counter += by
    counter = local_counter
    print(f'counter={counter}')

    lock.release()

lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f'The final counter is {counter}')

输出:

counter=10
counter=30
The final counter is 30

我们创建了一个全局共享变量 counter=0 和两个线程 t1t2。两个线程都针对相同的 increase() 函数。increase(by, lock) 函数有两个参数。第一个参数是它将增加 counter 的数量,第二个参数是 Lock 类的实例。除了前面的声明,我们还在 Python 的 threading 模块中创建了一个 Lock 类的实例 lockincrease(by, lock) 函数中的这个 lock 参数使用 lock.acquire() 函数锁定对 counter 变量的访问,同时它被任何线程修改,并使用 lock 解锁锁定。release() 函数,当一个线程修改了 counter 变量。线程 t1counter 的值增加 10,线程 t2counter 的值增加 20。

由于线程锁定,不会发生竞争条件,最终 counter 的值为 30。

在 Python 中使用 with lock: 的线程锁

前一种方法的问题在于,当一个线程完成处理时,我们必须小心地解锁每个锁定的变量。如果没有正确完成,我们的共享变量将只会被第一个线程访问,而其他线程将无法访问共享变量。这个问题可以通过使用上下文管理来避免。我们可以使用 with lock: 并将我们所有的关键代码放在这个块中。这是防止竞争条件的更简单的方法。以下代码片段显示了使用 with lock: 来防止 Python 中的竞争条件。

from threading import Thread, Lock

counter = 0

def increase(by, lock):
    global counter

    with lock:
        local_counter = counter
        local_counter += by
        counter = local_counter
    print(f'counter={counter}')

lock = Lock()

t1 = Thread(target=increase, args=(10, lock))
t2 = Thread(target=increase, args=(20, lock))

t1.start()
t2.start()

t1.join()
t2.join()

print(f'The final counter is {counter}')

输出:

counter=10
counter=30
The final counter is 30

我们将增加 counter 的代码放在 with lock: 块中。线程 t1counter 的值增加 10,线程 t2counter 的值增加 20。没有发生竞争条件,counter 的最终值为 30。此外,我们不需要担心解锁线程锁。

两种方法都完美地完成了它们的工作,即两种方法都可以防止竞争条件的发生,但是第二种方法远远优于第一种,因为它可以避免我们为处理线程锁的锁定和解锁而头疼。与第一种方法相比,它编写起来也更简洁,更易于阅读。

Muhammad Maisam Abbas avatar Muhammad Maisam Abbas avatar

Maisam is a highly skilled and motivated Data Scientist. He has over 4 years of experience with Python programming language. He loves solving complex problems and sharing his results on the internet.

LinkedIn

相关文章 - Python Thread