Sharing Data Safely
Use queues and locks.
Sharing Data Safely is a free Python Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Python Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Race Condition Problem
When multiple threads modify the same data, their operations can interleave and corrupt the result. This is a race condition. We need synchronization tools to share data safely.
import threading
counter = 0
def bump():
global counter
for _ in range(100000):
counter += 1
ts = [threading.Thread(target=bump) for _ in range(2)]
for t in ts: t.start()
for t in ts: t.join()
print('counter (may be < 200000):', counter)Lock to the Rescue
A threading.Lock ensures only one thread enters a critical section at a time. Acquire it with a with block around the shared update.
import threading
counter = 0
lock = threading.Lock()
def bump():
global counter
for _ in range(100000):
with lock:
counter += 1
ts = [threading.Thread(target=bump) for _ in range(2)]
for t in ts: t.start()
for t in ts: t.join()
print('counter:', counter)acquire and release
A lock can be used manually with acquire() and release(), but the with form is safer because it releases even if an error occurs.
import threading
lock = threading.Lock()
lock.acquire()
try:
print('inside critical section')
finally:
lock.release()
print('released')Thread-Safe Queues
The queue.Queue class is already thread-safe. Threads can put and get without explicit locks, making it the preferred way to pass data.
import queue, threading
q = queue.Queue()
def producer():
for i in range(3):
q.put(i)
t = threading.Thread(target=producer)
t.start()
t.join()
while not q.empty():
print('got', q.get())Producer and Consumer
The classic pattern: producers put work into a queue, consumers get and process it. The queue handles all the locking.
import queue, threading
q = queue.Queue()
results = []
def consumer():
while True:
item = q.get()
if item is None:
break
results.append(item * 10)
q.task_done()
t = threading.Thread(target=consumer)
t.start()
for i in range(3):
q.put(i)
q.put(None)
t.join()
print(results)join and task_done
q.join() blocks until every item that was put has had a matching task_done(). This lets the main thread wait for all work to finish.
import queue, threading
q = queue.Queue()
def worker():
while True:
item = q.get()
q.task_done()
threading.Thread(target=worker, daemon=True).start()
for i in range(5):
q.put(i)
q.join()
print('all items processed')RLock for Re-entrancy
A plain Lock deadlocks if the same thread tries to acquire it twice. A threading.RLock (re-entrant lock) allows that, counting acquisitions.
import threading
rlock = threading.RLock()
def outer():
with rlock:
inner()
def inner():
with rlock:
print('re-entered safely')
outer()Event for Signaling
A threading.Event lets one thread signal others. Threads call wait() until another calls set().
import threading
start = threading.Event()
def worker():
start.wait()
print('go!')
t = threading.Thread(target=worker)
t.start()
print('setting event')
start.set()
t.join()Sharing Across Processes
Across processes, use multiprocessing.Queue instead. It serializes objects between separate memory spaces.
import multiprocessing as mp
def producer(q):
for i in range(3):
q.put(i * i)
if __name__ == '__main__':
q = mp.Queue()
p = mp.Process(target=producer, args=(q,))
p.start()
p.join()
while not q.empty():
print('received', q.get())Avoiding Deadlock
Deadlock happens when threads wait on each other's locks forever. Avoid it by always acquiring multiple locks in the same order everywhere.
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def safe():
with lock_a:
with lock_b:
print('acquired in consistent order')
safe()Prefer Queues Over Locks
When possible, prefer message passing with queues over shared mutable state guarded by locks. It is easier to reason about and far less bug-prone.
import queue
q = queue.Queue()
for word in ['safe', 'simple', 'clear']:
q.put(word)
while not q.empty():
print(q.get())Quick Check
Test your understanding of safe data sharing.
Recap
You learned to share data safely:
- Race conditions corrupt unguarded shared state.
LockandRLockprotect critical sections.queue.Queueis thread-safe; usemultiprocessing.Queueacross processes.Eventsignals between threads; consistent lock order avoids deadlock.
Next course: running external programs with subprocess.
Frequently asked questions
Is the “Sharing Data Safely” lesson free?
Yes — the full text of “Sharing Data Safely” is free to read here on the web, and the Python Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Python Academy course, upgrade to CoddyKit PRO.
What will I learn in “Sharing Data Safely”?
Use queues and locks. You practise Python Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Python Academy?
No prior experience is required. Python Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Sharing Data Safely” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Python Academy lesson?
Yes. Every Python Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Threads and the GIL
- ThreadPoolExecutor
- multiprocessing Basics
- Sharing Data Safely