← Back to all articles

Process, Thread and Coroutine: Three Units of Concurrency

ConcurrencyBeginner

How they relate

UnitMemorySwitch costCommunication
ProcessIsolatedHigh (kernel)IPC / pipes
ThreadShared addressMediumShared memory, lock
CoroutineShared threadLow (user space)Channels / vars

Why coroutines are lighter

Threads are scheduled by the OS; thousands of them burn CPU on context switches. Coroutines are scheduled by the runtime in user space, so one thread can host tens of thousands and yield on waits (I/O) without touching kernel resources.

Costs and traps

  1. Lock shared memory: threads and coroutines share variables, so races cause weird bugs;
  2. Do not compute heavy CPU in a coroutine: blocking the scheduler stalls its siblings;
  3. Parallel is not concurrent: multiple cores run in parallel; one core only switches.

One-line choice

CPU-bound: multiprocess or multithread to use cores. I/O-bound: coroutines or async to serve high concurrency with few threads.

Testing and observing concurrent programs

Concurrency bugs are hard to reproduce, so testing and observability matter more than coding tricks.

  1. Make concurrency explicit: shared state, critical sections and lock ordering should be visible at a glance. If it takes a long read to judge whether something is thread-safe, the design is too implicit, refactor to a clearer boundary.
  2. Test with injectable scheduling: make waiting and scheduling dependencies you can control, then force interleavings deterministically to reproduce check-then-act races instead of hoping random timing hits them.
  3. Load-test bursts: concurrency problems surface under sudden load, so include both ramp-up and instantaneous spikes, watching queue depth, rejection rate and latency percentiles rather than average throughput.
  4. Observe threads and coroutines: export thread stacks, coroutine counts and queue lengths with threshold alerts. Steadily growing coroutine counts usually mean blocking calls crept into an async path or tasks never complete.
  5. Define backpressure: when producers outrun consumers you must choose explicitly, throttle, drop or buffer. Systems without backpressure fail in unpredictable ways under pressure.

Concurrency and resources

More concurrency is not better: each unit consumes memory and handles and competes for CPU. Evaluate concurrency alongside resource limits and downstream capacity to find a value that is neither overloading nor idle.

Debugging async code

Async call stacks are usually incomplete, making the origin hard to find. Attach a traceable context identifier to each task and log it consistently, and preserve the original stack when handling exceptions rather than losing it through layers of wrapping. These habits cut debugging time noticeably.