Concurrency Design Patterns

Patterns for multi-threaded programming: Producer-Consumer, Reader-Writer, Thread Pool, Future/Promise, Reactor, Monitor, Scheduler.

The data

Patterns

NameProblemSolution briefKey componentsBenefitsPitfallsLanguage examples
Producer-Consumer (Bounded Buffer)Producers generate data; consumers process it. Need to coordinate so consumers don't read from empty buffer, producers don't write to full buffer.Use a fixed-size queue as buffer. Producers block when full; consumers block when empty. Synchronized access ensures thread safety.
  • Shared buffer/queue with bounded capacity
  • Producer threads (generate items, put in buffer)
  • Consumer threads (take items from buffer, process)
  • Synchronization mechanism (mutex + condition variables/semaphores)
Decouples producers and consumers, smooths load variations, enables pipelining, buffers spikes in production/consumption rates.Deadlock if synchronization incorrect, priority inversion possible, buffer size tuning critical (too small causes blocking; too large uses memory), consumers may starve if producers fast.
Java
BlockingQueue<E> interface (ArrayBlockingQueue, LinkedBlockingQueue); put() and take() block automatically.
Python
queue.Queue (thread-safe), producer calls put(), consumer calls get()
C++
std::queue with std::mutex + std::condition_variable, or boost::lockfree::queue
Go
Channels (chan) with buffered or unbuffered; goroutines communicate naturally
Reader-Writer LockData structure frequently read, rarely written. Mutex allows only one thread at a time (even for readers), causing unnecessary contention.Allow multiple readers simultaneous access, but writers get exclusive access. Readers don't block each other; writers block both readers and other writers.
  • Read lock (shared): multiple threads can hold simultaneously
  • Write lock (exclusive): only one thread, no readers
  • Lock upgrade/downgrade (optional)
  • Reader counter or reference count
Better throughput for read-heavy workloads, scales well with many readers, writers get exclusive access to prevent dirty reads.Writer starvation if continuous readers arrive, reader-writer deadlock if not careful, higher overhead than simple mutex for write-heavy cases, complex to implement correctly.
Java
java.util.concurrent.locks.ReentrantReadWriteLock; readLock().lock(), writeLock().lock()
C++
std::shared_mutex (C++17); std::shared_lock for readers, std::unique_lock for writers
Python
threading.RLock doesn't support readers; use read_write_lock third-party or multiprocessing.Manager
C#
ReaderWriterLockSlim; EnterReadLock(), EnterWriteLock()
Thread PoolCreating/destroying threads is expensive (OS resources, context switching). Spawning thread per task leads to overhead and uncontrolled resource usage.Pre-create a fixed number of worker threads and queue tasks. Workers wait for tasks; when task arrives, idle worker executes it. Reuses threads across many tasks.
  • Thread pool (fixed size worker threads)
  • Task queue (work queue, blocking)
  • Worker loop: while (true) { task = queue.take(); execute(task); }
  • Saturation strategy (queue full: reject, block, or spawn new thread)
Reduces thread creation overhead, bounds resource usage, improves response time (threads ready), enables task prioritization and scheduling.Queue can become bottleneck, thread starvation if pool too small, deadlocks if tasks wait on other queued tasks, tasks may block pool threads indefinitely.
Java
ExecutorService, ThreadPoolExecutor, Executors.newFixedThreadPool(n), CompletableFuture with custom executor
Python
concurrent.futures.ThreadPoolExecutor, submit(task) returns Future
C#
Task Parallel Library (TPL), ThreadPool.QueueUserWorkItem, Task.Run
C++
Boost.Asio thread pool, Intel TBB task scheduler, custom with std::thread + std::queue
Future / PromiseNeed to run computation asynchronously and retrieve result later. Caller shouldn't block waiting for result; needs way to check completion or get result when ready.Future is a placeholder for result of async operation. Promise is writeable handle that fulfills the future. Caller gets future immediately, checks isDone(), calls get() to block or poll.
  • Future (read-only): isDone(), get(timeout), cancel()
  • Promise (write-only): setValue(result), setException(err)
  • Completion callback (optional): then(), addListener()
  • Cancellation mechanism
Avoids blocking threads, enables composition of async operations, simplifies error propagation, clean API for async results.Blocking get() defeats purpose (use callbacks instead), exceptions must be captured in Future, cancellation not always supported, many small async tasks create overhead.
Java
CompletableFuture<T> (most powerful), Future<T> (older), CompletableFuture.supplyAsync(...)
Java script
Promise (then/catch), async/await syntactic sugar
Python
concurrent.futures.Future, asyncio.Future
C#
Task<T> (Task is Future without result); async/await
Reactor PatternServer handling many simultaneous connections (e.g., web server). One thread per connection (thread-per-connection) doesn't scale (thousands of connections = thousands of threads).Single (or few) thread(s) use non-blocking I/O and demultiplexer (select/poll/epoll/kqueue) to monitor multiple sockets. When I/O event occurs, dispatcher calls appropriate handler callback.
  • Event demultiplexer (select, poll, epoll, kqueue)
  • Event handlers (callback objects)
  • Reactor (main loop: wait for events -> dispatch to handlers)
  • Non-blocking I/O sockets
Scales to thousands of connections with few threads, efficient resource usage (no thread overhead), good for I/O-bound services.Complex to implement correctly, callbacks can lead to spaghetti code (use coroutines/fibers), CPU-bound tasks still block reactor, edge-triggered vs level-triggered pitfalls.
Java
NIO (Non-blocking I/O), Selector, SocketChannel; Netty framework implements Reactor
Python
selectors module, asyncio event loop, Twisted framework
Node.js
libuv event loop (epoll/kqueue/IOCP) built-in; all I/O non-blocking
C++
Boost.Asio, libevent, libuv
Monitor (Monitor Object)Multiple threads access shared data; need to ensure mutual exclusion and coordinate access. Condition variables and mutexes scattered lead to error-prone code.Encapsulate shared data with its synchronization (mutex) and condition variables inside a monitor object. Only one thread can execute any monitor method at a time. Condition variables allow threads to wait for conditions.
  • Mutual exclusion lock (implicit in monitor entry)
  • Condition variables (wait/signal)
  • Shared data (private to monitor)
  • Procedures/methods (only one active at a time)
Simplifies synchronization (acquire/release implicit), prevents unsynchronized access (all accesses go through monitor), high-level abstraction, reduces race conditions.Can cause performance bottleneck if monitor too large (coarse-grained), conditional waiting can lead to missed signals or spurious wakeups, deadlocks possible if monitor calls external code.
Java
synchronized methods/blocks (built-in monitor), wait()/notify()/notifyAll() on object
Python
threading.Lock + Condition, but no built-in monitor keyword; can use with statement
C#
lock statement (Monitor.Enter/Exit), Monitor.Wait/Pulse
C++
No built-in monitor; implement with std::mutex + std::condition_variable
Scheduler PatternNeed to execute tasks at specific times (cron jobs), at fixed intervals, or after delays. Managing timers and thread pools manually is complex.Central scheduler coordinates task execution based on time criteria. Maintains priority queue of scheduled tasks; scheduler thread manages timers and dispatches tasks to worker threads when due.
  • Task (callable with execution time, priority, periodicity)
  • Schedule queue (priority queue ordered by next execution time)
  • Timer mechanism (wait/notify, delay queue, wheel timer)
  • Worker pool (optional, execute tasks asynchronously)
  • Scheduling strategies (fixed-rate, fixed-delay, cron)
Centralized task management, efficient timer management (single thread for many timers), supports recurring tasks, cron-like scheduling, easy to pause/cancel/reschedule.Scheduler thread single point of failure, task overruns can backlog, clock drift affects timing, distributed systems need distributed scheduler (e.g., Quartz, Celery).
Java
ScheduledExecutorService, Timer/TimerTask, Quartz Scheduler, Spring @Scheduled
Python
sched module, APScheduler, Celery beat, cron jobs via OS
C#
System.Threading.Timer, Quartz.NET, Hangfire
Node.js
node-cron, setInterval/setTimeout (single-threaded)

Fetch the same bytes

The static files are identical to what the API returns, but with no rate limit and no server round trip. Use the API when you want a query and a content type; use the files when you want to cache one document.

curl "https://yjtoon.com/api/dataset/concurrency-design-patterns?format=toon"
const res = await fetch(
  "https://yjtoon.com/static-data/dataset/concurrency-design-patterns.toon"
);
const toon = await res.text();

Rate limit: 120 requests per minute per IP, no key and no signup. API reference →

Topics

  • design-patterns
  • concurrency
  • multithreading
  • producer-consumer
  • thread-pool