Python Threads - Critical Section

后端 未结 3 756
走了就别回头了
走了就别回头了 2020-12-31 11:11

What is the \"critical section\" of a thread (in Python)?

A thread enters the critical section by calling the acquire() method, which can either b

3条回答
  •  长情又很酷
    2020-12-31 11:38

    Other people have given very nice definitions. Here's the classic example:

    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
    

    Let's say that the += operator consists of three subcomponents:

    • Read the current value
    • Add the RHS to that value
    • Write the accumulated value back to the LHS (technically bind it in Python terms)

    If you don't have the with account_balance_lock statement and you execute two change_account_balance calls in parallel you can end up interleaving the three subcomponent operations in a hazardous manner. Let's say you simultaneously call change_account_balance(100) (AKA pos) and change_account_balance(-100) (AKA neg). This could happen:

    pos = threading.Thread(target=change_account_balance, args=[100])
    neg = threading.Thread(target=change_account_balance, args=[-100])
    pos.start(), neg.start()
    
    • pos: read current value -> 0
    • neg: read current value -> 0
    • pos: add current value to read value -> 100
    • neg: add current value to read value -> -100
    • pos: write current value -> account_balance = 100
    • neg: write current value -> account_balance = -100

    Because you didn't force the operations to happen in discrete chunks you can have three possible outcomes (-100, 0, 100).

    The with [lock] statement is a single, indivisible operation that says, "Let me be the only thread executing this block of code. If something else is executing, it's cool -- I'll wait." This ensures that the updates to the account_balance are "thread-safe" (parallelism-safe).

    Note: There is a caveat to this schema: you have to remember to acquire the account_balance_lock (via with) every time you want to manipulate the account_balance for the code to remain thread-safe. There are ways to make this less fragile, but that's the answer to a whole other question.

    Edit: In retrospect, it's probably important to mention that the with statement implicitly calls a blocking acquire on the lock -- this is the "I'll wait" part of the above thread dialog. In contrast, a non-blocking acquire says, "If I can't acquire the lock right away, let me know," and then relies on you to check whether you got the lock or not.

    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()
    

    I also want to add that the lock's primary purpose is to guarantee the atomicity of acquisition (the indivisibility of the acquire across threads), which a simple boolean flag will not guarantee. The semantics of atomic operations are probably also the content of another question.

提交回复
热议问题