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.