Concurrency

It is the ability of a program to manage multiple tasks at the same time.Concurrency doesn’t mean these tasks are happening at the same time(like in parallel processing) but rather that they are interleaved or coordinated to make progress together.

Switching between tasks

Programs switch tasks using mechanisms like threads, processes and event loops.

Process: An independent unit of execution each with it’s own memory space and is managed by the operating system. Processes connect via interprocess communication mechanisms like pipes, sockets or shared files. They are heavy weight due to memory isolation but are safe. Threads: It’s a lightweight execution unit within a process sharing the same memory space. They are managed by the operating system and are cheaper to create than processes. However, shared memory introduces risks like race conditions. Event loop: A single threaded mechanism that multiplexes tasks by processing a queue of events. The event loop checks for pending events, runs their handlers and yields control while tasks wait.

The operating system’s scheduler decides which task runs when, using context switching (saving and restoring task state), and for threads and processes, the scheduler uses preemption (pausing a task to run another) which are based on priorities and timeslices.

Preventing Chaos when threads share data

Shared memory creates race conditions where the outcome depends on unpredictable execution order.

Locks(mutexes): Ensure only one thread can access data at a time. It acts like a key locking the resource when it is using it preventing access by other threads. After it is finished it unlocks it. Semaphore: A semaphore is a more flexible synchronization mechanism than a mutex. It maintains an internal counter representing the number of available resources. Threads can wait on the semaphore (decrementing the counter) and if the counter becomes negative the thread blocks until another thread signals (increments the counter)

These primitives are implemente using OS-level constructs or hardware instructions. However, they introduce overhead and risks like priority inversion, where a low priority thread holding a lock delays a high-priority one.

Problems with locks

Deadlocks: This where two locks are stuck waiting for a resource another holds forming a circular dependency. Livelocks: Threads are active but make no progress because they keep responding to each others actions. Starvation: A thread is denied access to resource as other threads keep acquiring the lock.

Preventing the problems

  1. Always acquire locks in a consitent order to prevent deadlocks. This strategy is known as lock ordering.
  2. We can use timeouts which sets a deadline for lock acquisition, releases if the lock isn’t acquired in time
  3. Use non-blocking lock attempts to avoid waiting idefinitely or use logic which returns immediately if the lock is unavailable.

Coordinating tasks without locks

Lockfree programming which uses atomic operations, which are hardware supported instructions that complete in a single, uninterruptible step. The most common atomic operations are:

  • Compare and swap: checks if a variable has an expected value and updates it if so, atomically.
  • Fetch and add: increments a variable and returns its old value.
  • Test and Set: sets a flag and returns its previous state.

Message passing: Message passing avoids shared memory entirely by having tasks communicate via messages. Each task(or “actor”) has its own state and processes messages from a queue, eliminating race conditions.

← back to index