【问题标题】:Python Threads - Critical SectionPython 线程 - 关键部分
【发布时间】:2009-01-07 03:58:18
【问题描述】:

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

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

-Understanding Threading in Python, Linux Gazette

另外,锁的用途是什么?

【问题讨论】:

    标签: python multithreading language-agnostic


    【解决方案1】:

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

    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)(AKA pos)和change_account_balance(-100)(AKA 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
    • 否定:将当前值添加到读取值 -> -100
    • pos: 写入当前值 -> account_balance = 100
    • 否定:写入当前值 -> account_balance = -100

    因为您没有强制操作在离散块中发生,所以您可以有三种可能的结果(-100、0、100)。

    with [lock] 语句是一个单一的、不可分割的操作,它说:“让我成为执行此代码块的唯一线程。如果正在执行其他代码,那很酷——我会等。”这确保了对account_balance 的更新是“线程安全的”(并行安全)。

    注意:此架构有一个警告:您必须记住每次要操作 account_balance 以获取 account_balance_lock(通过 with)代码保持线程安全。有一些方法可以减少这种脆弱性,但这是另一个问题的答案。

    编辑:回想起来,提到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 跨线程的不可分割性),一个简单的布尔标志不能保证。原子操作的语义大概也是另外一个问题的内容。

    【讨论】:

      【解决方案2】:

      代码的关键部分是一次只能由一个线程执行的代码。以聊天服务器为例。如果每个连接(即每个最终用户)都有一个线程,则一个“关键部分”是假脱机代码(向所有客户端发送传入消息)。如果有多个线程同时尝试对一条消息进行假脱机,您将得到 BfrIToS MANtwD PIoEmesCEsaSges 交织在一起,这显然是没有好处的。

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

      【讨论】:

      • -1 用于传播一个非常错误和糟糕的设计选择:丑陋可怕的 每个连接一个线程 方法很常见但错误。
      • 试着告诉 Erlang 的人。在许多编程语言中它可能是错误的,但由于它如此普遍(并提供了如此有用的示例),我决定使用它。在关于连接池的问题中,我会说别的:)
      【解决方案3】:

      "critical section" 是一段代码,为了正确起见,必须确保该部分中一次只能有一个控制线程。一般来说,您需要一个临界区来包含将值写入到内存中的引用,这些引用可以在多个并发进程之间共享。

      【讨论】:

      • 新手可能会对您的建议感到困惑。听起来您说可以从多个线程共享的内存中读取而无需锁定,当然不是(除非您确定写入是原子的)。
      • 我不确定我们是否存在分歧。 只要写入是原子的,读取就很好。每个人都可以阅读 const 而无需关键部分。
      • 是的,但是如果您正在写入数据数组、列表或字典,而另一个线程正在读取它,则可能会在中间状态下读取它,除非读取是“原子”的同样,使用相同的信号量/锁。
      • 这当然是由于对数组等的访问不是原子的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-14
      • 2011-01-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多