Python线程 - 临界区

16

什么是线程(Python中)的“临界区”?

线程通过调用 acquire() 方法进入临界区,该方法可以是阻塞或非阻塞的。线程通过调用 release() 方法退出临界区。

- 了解 Python 中的线程,Linux Gazette

此外,锁的目的是什么?

3个回答

25

其他人已经给出了非常好的定义。这里是一个经典的例子:

import threading
account_balance = 0 # The "resource" that zenazn mentions.
account_balance_lock = threading.Lock()

def change_account_balance(delta):
    global account_balance
    with account_balance_lock:
        # Critical section is within this block.
        account_balance += delta

假设 += 操作符由三个子组件组成:
  • 读取当前值
  • 将右操作数(RHS)添加到该值中
  • 将累加的值写回左操作数(LHS)(在Python术语中技术上称为绑定)
如果您没有使用 with account_balance_lock 语句并且同时执行两个 change_account_balance 调用,则可能以危险的方式交错三个子组件操作。 假设您同时调用 change_account_balance(100)(也称为pos)和change_account_balance(-100)(也称为neg)。 这可能会发生:
pos = threading.Thread(target=change_account_balance, args=[100])
neg = threading.Thread(target=change_account_balance, args=[-100])
pos.start(), neg.start()
  • pos:读取当前值 -> 0
  • neg:读取当前值 -> 0
  • pos:将当前值加到读取的值中 -> 100
  • neg:将当前值加到读取的值中 -> -100
  • pos:写入当前值 -> account_balance = 100
  • neg:写入当前值 -> account_balance = -100

由于您没有强制操作以离散块的形式进行,因此可能会有三种可能的结果(-100、0、100)。

with [lock]语句是一种单独不可分割的操作,它表示:“让我是唯一执行此代码块的线程。如果其他线程正在执行,则没问题——我会等待。”这确保了对account_balance的更新是“线程安全的”(并行安全的)。

注意:这种模式有一个警告:您必须记住每次想要操作account_balance时通过with获取account_balance_lock,以使代码保持线程安全。有办法使其变得不那么脆弱,但那是另一个问题的答案。

编辑:回想起来,可能重要的是提到with语句隐式调用锁定的阻塞acquire - 这是上面线程对话框中的“我会等待”部分。相比之下,非阻塞获取说:“如果我不能立即获得锁,请告诉我”,然后依赖于您检查是否已经获得了锁。

import logging # This module is thread safe.
import threading

LOCK = threading.Lock()

def run():
    if LOCK.acquire(False): # Non-blocking -- return whether we got it
        logging.info('Got the lock!')
        LOCK.release()
    else:
        logging.info("Couldn't get the lock. Maybe next time")

logging.basicConfig(level=logging.INFO)
threads = [threading.Thread(target=run) for i in range(100)]
for thread in threads:
   thread.start()

我想补充一点,锁的主要目的是确保获取的原子性(即在线程之间不可分割的acquire),而简单的布尔标志无法保证。原子操作的语义可能也是另一个问题的内容。

18

代码的关键部分是只能由一个线程执行的部分。以聊天服务器为例,如果您为每个连接(即每个终端用户)创建一个线程,则“关键部分”是Spooling代码(将传入消息发送给所有客户端)。如果多个线程同时尝试排队一条消息,则会得到交织在一起的BfrIToS mANtwD PIoEmesCEsaSges,这显然是不好的。

锁是可用于同步访问关键部分(或资源总体)的东西。在我们的聊天服务器示例中,锁就像带有打字机的锁定房间。如果有一个线程在里面(以便将一条消息打出来),则其他线程无法进入该房间。一旦第一个线程完成,他解锁该房间并离开。然后,另一个线程可以进入该房间(锁定它)。“获取”锁只意味着“我得到了房间”。


1
-1 是因为采用了一个非常错误和糟糕的设计选择:丑陋可怕的“每个连接一个线程”的方法,这种方法虽然常见但是是错误的。 - nosklo
5
试着告诉那些Erlang的人吧。虽然这种方式在许多编程语言中是错误的,但由于它非常普遍(并且提供了如此有用的示例),所以我决定采用这种方式。如果是关于连接池的问题,我会说其他的话 :) - zenazn

0

"关键区段"是一段代码,为了正确性,必须确保在同一时间只有一个控制线程可以进入该区段。通常,您需要一个关键区段来包含引用,这些引用将值写入到可以在多个并发进程之间共享的内存中。


一个新手可能会被你的建议所困惑。听起来好像你在说从多个线程共享的内存中读取而不加锁是可以的,当然这是不对的(除非你确定你的写入是原子的)。 - mhenry1384
我不确定我们是否存在分歧。只要写操作是原子的,读操作就没问题。每个人都可以在没有需要临界区的情况下读取常量。 - Charlie Martin
是的,但如果您正在编写数据数组、列表或字典,而另一个线程正在读取它,则除非读取也是“原子性”的,使用相同的信号量/锁,否则可能会以中间状态进行读取。 - MrWonderful
当然,这是由于访问数组等不是原子性的结果。 - Charlie Martin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接