synchronization algorithms
distributed systems
data consistency
computing
real-time systems

Synchronisation algorithms

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Synchronisation algorithms coordinate concurrent work so shared state stays correct and threads or processes make progress in a controlled order. The right algorithm depends on what you need to protect: a critical section, a limited resource pool, a producer-consumer queue, or a distributed system where machines must agree on event order.

Mutual Exclusion: One Thread at a Time

The most basic synchronisation problem is mutual exclusion. If two threads update the same variable at the same time, the final result may be wrong because the read and write steps interleave unpredictably.

A lock solves that by letting only one thread enter the critical section at a time.

python
1import threading
2
3counter = 0
4lock = threading.Lock()
5
6
7def increment():
8    global counter
9    for _ in range(100_000):
10        with lock:
11            counter += 1
12
13
14threads = [threading.Thread(target=increment) for _ in range(4)]
15
16for thread in threads:
17    thread.start()
18
19for thread in threads:
20    thread.join()
21
22print(counter)

Without the lock, the final value can be lower than expected because increments are not atomic. With the lock, the result is deterministic.

Conceptually, this is the same family of problem addressed by classic algorithms such as Peterson's algorithm, Dekker's algorithm, and bakery-style ticket algorithms. Those are historically important because they show how mutual exclusion can be achieved even without high-level locks, but in most production code you use the synchronization primitives provided by the language runtime or operating system.

Semaphores: Limited Parallel Access

Sometimes you do not want exclusive access. You want to allow a fixed number of workers through at once. That is where semaphores fit.

Imagine a service that should allow only two concurrent downloads:

python
1import threading
2import time
3
4slots = threading.Semaphore(2)
5
6
7def download(name: str) -> None:
8    with slots:
9        print(f"{name} started")
10        time.sleep(1)
11        print(f"{name} finished")
12
13
14threads = [threading.Thread(target=download, args=(f"job-{i}",)) for i in range(5)]
15
16for thread in threads:
17    thread.start()
18
19for thread in threads:
20    thread.join()

The semaphore acts like a counter. As long as capacity remains, a thread may proceed. When the count reaches zero, later threads wait.

This pattern is useful for connection pools, worker throttling, and rate-limited resources where total exclusion would be too restrictive.

Coordination Algorithms for Order, Not Just Safety

Synchronisation is not only about preventing collisions. It is also about controlling when certain steps may happen. Condition variables, events, and barriers coordinate progress between tasks.

A producer-consumer example shows this clearly. The consumer should wait until data exists rather than polling constantly.

python
1import threading
2
3items = []
4condition = threading.Condition()
5
6
7def producer():
8    for value in [10, 20, 30]:
9        with condition:
10            items.append(value)
11            condition.notify()
12
13
14def consumer():
15    for _ in range(3):
16        with condition:
17            while not items:
18                condition.wait()
19            print(f"Consumed {items.pop(0)}")
20
21
22producer_thread = threading.Thread(target=producer)
23consumer_thread = threading.Thread(target=consumer)
24
25consumer_thread.start()
26producer_thread.start()
27
28producer_thread.join()
29consumer_thread.join()

The important detail is the while loop around condition.wait(). Waking up does not guarantee the condition is still true by the time the thread resumes, so correct waiting logic rechecks the state.

Distributed Synchronisation Is a Different Class of Problem

Inside one process, threads can share memory and use locks. In distributed systems, machines do not share a single memory space, network delays exist, and clocks disagree. That changes the problem entirely.

Common distributed synchronisation strategies include:

  • logical clocks to order events,
  • leader election to coordinate decisions,
  • consensus algorithms to agree on state changes.

For example, Lamport clocks help reason about event ordering when wall-clock timestamps are not trustworthy enough. Raft and Paxos address a harder question: how multiple nodes can agree on one value even when failures occur.

Those algorithms are much more expensive than a local mutex because they involve messaging, persistence, and failure handling. That is why you should not casually apply distributed coordination when a local synchronization primitive would solve the actual problem.

Choosing the Right Primitive

A practical way to choose:

  • Use a lock for one shared critical section.
  • Use a semaphore when you want bounded concurrency.
  • Use a condition variable, event, or barrier when threads must coordinate phases of work.
  • Use distributed algorithms only when state or coordination spans multiple machines.

The choice matters because the wrong primitive either hurts performance or fails to express the real constraint. A lock around an entire pipeline may serialize too much work. A semaphore may allow progress when you actually needed strict ordering. A distributed coordinator may add large complexity where a local queue would have been enough.

Common Pitfalls

  • Protecting shared data inconsistently, so some code paths use a lock and others do not.
  • Holding a lock longer than necessary and reducing concurrency more than intended.
  • Using busy waiting instead of proper condition-based blocking.
  • Forgetting that distributed synchronization problems are fundamentally different from in-process ones.
  • Assuming a synchronization primitive fixes a design that still has unclear ownership of shared state.

Summary

  • Synchronisation algorithms coordinate concurrent work so data stays correct and execution order stays meaningful.
  • Locks handle mutual exclusion for critical sections.
  • Semaphores allow a limited number of concurrent users of the same resource.
  • Condition-style primitives coordinate state transitions and execution phases.
  • Distributed synchronisation requires different algorithms because machines do not share memory or perfectly synchronized clocks.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.