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