Programming Fundamentals and Object-Oriented Programming

Chapter 14
Concurrency, Asynchronous Programming and Performance

Post Graduate Diploma in Full Stack Development
Programming Fundamentals and Object-Oriented Programming   •   Post Graduate

14. Concurrency, Asynchronous Programming and Performance

Introduction and Context

Modern software rarely performs one operation in isolation. A web server may receive thousands of requests while reading files, querying databases, calling external services, and recording logs. A mobile application must remain responsive while downloading data. A data-processing system may need to use several processor cores simultaneously. These requirements make concurrency and performance central concerns in professional software development.

Concurrency is the ability of a system to make progress on more than one task during an overlapping period. Parallelism is the simultaneous execution of multiple tasks, usually on different processor cores. Asynchronous programming is a style in which a program initiates an operation and continues with other work rather than waiting synchronously for that operation to finish. These ideas are related, but they are not interchangeable. A program can be concurrent without being parallel, and asynchronous code does not automatically execute on multiple cores.

Poorly designed concurrent programs can be difficult to understand and may produce intermittent failures. Race conditions, deadlocks, starvation, unsafe shared state, excessive context switching, and unbounded queues are common causes of failure. Consequently, performance work must combine programming knowledge with measurement, operational observation, and disciplined design.

Learning Objectives

After studying this chapter, students should be able to:

  • distinguish processes, threads, concurrency, parallelism, and asynchronous execution;
  • explain the operation of event loops, futures, promises, and asynchronous tasks;
  • identify race conditions, deadlocks, starvation, livelocks, and thread-safety problems;
  • select suitable synchronization and communication techniques;
  • interpret latency, throughput, utilization, saturation, and related performance metrics;
  • use profiling and monitoring evidence to guide optimization;
  • evaluate concurrency designs in terms of correctness, scalability, maintainability, and operational risk.

Processes, Threads and the Execution Model

A process is an independent execution environment managed by the operating system. It normally has its own address space, resources, and security boundary. Communication between processes requires mechanisms such as pipes, sockets, files, shared memory, or message queues. Because processes are relatively isolated, a failure in one process is less likely to corrupt the memory of another, although process creation and communication may be comparatively expensive.

A thread is an execution path within a process. Threads in the same process share memory and many resources, which makes communication efficient but creates risks. If two threads access the same mutable data without suitable coordination, the result may depend on timing. Threads are therefore powerful but require careful management of shared state.

Concurrency concerns the structure of a program: several tasks can be in progress at once. On a single-core processor, the operating system can create this effect by rapidly switching between tasks. Parallelism concerns actual simultaneous execution. A multi-threaded program may achieve parallelism when runnable threads are scheduled on separate cores, subject to language runtime constraints and available hardware.

A useful classification is based on the nature of work. I/O-bound work spends substantial time waiting for disks, networks, databases, or other external systems. Concurrency can improve such systems by allowing other tasks to run during waiting periods. CPU-bound work spends most of its time performing computation. It may benefit from parallel execution, optimized algorithms, vectorization, native extensions, or additional machines. Adding threads to a CPU-bound program does not necessarily improve performance and may increase scheduling overhead.

Asynchronous Programming and the Event Loop

Synchronous code normally waits for an operation to complete before moving to the next statement. In asynchronous code, an operation can return a future, promise, task, or callback representing a result that will become available later. The program can then perform other work and respond when the result is ready.

An event loop is a control structure that repeatedly checks for completed I/O, timers, messages, or scheduled callbacks and dispatches the corresponding handlers. In a typical single-threaded event-loop system, the application thread does not block while an operating-system or library component performs network or file I/O. When the operation completes, the event loop schedules its continuation.

For example, conceptually:

```text

result = await database_query()

process(result)

```

The `await` operation appears sequential, which improves readability, but it normally suspends the current task rather than blocking the entire event-loop thread. Other tasks can execute while the query is pending. This benefit disappears if the event loop performs long-running CPU work or calls a blocking library directly.

Asynchronous execution can be represented by a state machine. A task begins, reaches an incomplete operation, stores its continuation, and yields control. When the operation completes, the continuation is placed on a queue. The event loop later resumes it. This model explains why asynchronous programs need careful error handling: failures may be stored in a future or delivered through a callback rather than raised immediately at the original call site.

Concurrency may be expressed through several approaches:

  • Callbacks: a function is supplied to run after completion. Callbacks are flexible but can become difficult to compose.
  • Promises or futures: an object represents a value or failure that will be available later. They support composition and explicit error propagation.
  • Async/await: a language-level syntax for writing asynchronous control flow in a sequential style.
  • Actors and message passing: independent units communicate through messages rather than sharing mutable memory.
  • Reactive streams: values are delivered over time, with operators for transformation, combination, and flow control.

A central concern is backpressure. If producers generate work faster than consumers can process it, an unbounded queue can consume memory and increase latency. Systems should define queue limits, rejection policies, batching strategies, or throttling mechanisms.

Synchronization and Safe Shared State

Synchronization coordinates access to data or resources. A mutex or lock allows only one thread at a time to enter a critical section. A semaphore controls access to a limited number of permits, such as database connections. A condition variable allows a thread to sleep until a state condition becomes true. Barriers coordinate groups of threads that must reach a stage before any proceeds.

Atomic operations provide indivisible updates to particular values. A counter increment implemented as a read followed by a write is not necessarily atomic: two threads may read the same old value and overwrite one another’s updates. An atomic increment or a lock can prevent this lost-update problem.

Thread safety means that a component behaves correctly when used concurrently under its documented contract. It may be achieved through immutability, confinement, synchronization, thread-safe data structures, or message passing. Immutability is especially valuable because an object whose state cannot change after construction can be shared without protecting every read.

The most reliable design is often to minimize shared mutable state. Instead of allowing several workers to modify one complex object, a system can give each worker independent data and combine results in a controlled phase. Message passing and actor-based designs make ownership explicit and reduce direct memory races.

Failure Modes in Concurrent Programs

A race condition occurs when the outcome depends on the relative timing of operations. A data race is a specific form in which multiple execution contexts access the same memory concurrently, at least one access is a write, and no adequate synchronization establishes ordering. Race conditions can also occur in distributed systems involving messages, files, or external resources.

A deadlock occurs when tasks wait indefinitely for one another. A classic example is thread A holding lock X and waiting for lock Y while thread B holds lock Y and waits for lock X. Deadlock is commonly explained through four necessary conditions: mutual exclusion, hold and wait, no pre-emption, and circular wait. Breaking any one of these conditions can prevent deadlock.

Practical prevention techniques include acquiring locks in a globally consistent order, avoiding unnecessary nested locks, using timeouts, keeping critical sections short, and preferring higher-level concurrency abstractions. Lock ordering must be documented and tested; otherwise, a later code change can reintroduce circular waiting.

Starvation occurs when a task waits indefinitely because other tasks repeatedly receive resources or scheduling opportunities. Livelock occurs when tasks remain active but make no useful progress, often because they repeatedly detect conflict and retry. Fair scheduling, bounded retries, randomized backoff, and resource quotas can reduce these problems.

A related issue is priority inversion, in which a high-priority task waits for a resource held by a lower-priority task while medium-priority tasks continue to run. Priority inheritance and careful priority design are possible remedies in systems where scheduling priorities matter.

Models for Reasoning About Concurrency

The producer-consumer model separates work creation from work processing. Producers place jobs into a queue, and consumers remove and execute them. The model supports workload smoothing, but it requires capacity limits and a policy for overload. A thread pool implements a bounded group of workers and is usually safer than creating one thread for every request.

The fork-join model divides a task into subtasks, executes them concurrently, and combines their results. It is suitable for independent computations such as parallel searches or data transformations. Its efficiency depends on task granularity: tasks that are too small create scheduling overhead, while tasks that are too large limit parallelism.

The pipeline model divides processing into stages. Each stage can work on a different item simultaneously. Pipelines improve throughput when stages are balanced, but a slow stage becomes a bottleneck. Buffering between stages can absorb short fluctuations but cannot eliminate sustained capacity limitations.

Amdahl’s Law provides a useful warning about parallel speed-up. If a fraction of a program must remain sequential, increasing the number of processors eventually produces diminishing returns. The practical lesson is that optimization should identify the dominant serial and parallel portions rather than assuming that more workers always produce proportional improvement.

Performance Measurement and Profiling

Performance should be described using measurable indicators. Latency is the time required to complete an operation. Average latency can conceal serious user experiences, so percentile latency—such as p95 or p99—is often more informative. Throughput is the amount of work completed per unit of time, such as requests per second. Utilization describes how much of a resource is busy. Saturation indicates that demand is approaching or exceeding capacity. Error rate, queue depth, timeout rate, and retry count are also important operational measures.

The relationship between concurrency and response time is not linear. As workload increases, a system may initially use idle capacity effectively. Near saturation, queues grow and latency rises sharply. This is why a service can maintain acceptable average performance while tail latency becomes unacceptable.

Profiling identifies where time, memory, or synchronization effort is spent. CPU profilers can use sampling, which periodically records the active stack, or instrumentation, which records selected events. Sampling usually has lower overhead; instrumentation can provide detailed timings but may alter behavior. Memory profilers identify allocation patterns and retained objects. Lock profilers reveal contention and waiting time. Tracing connects operations across services using correlation identifiers.

Optimization should follow a disciplined cycle:

1. define a performance objective and workload;

2. establish a baseline using representative measurements;

3. profile to locate a bottleneck;

4. change one important factor;

5. benchmark and compare results statistically;

6. test correctness, capacity, and failure behavior;

7. monitor the change in production.

A benchmark that uses unrealistic data, a warm cache, no contention, or an empty database may produce misleading conclusions. Performance tests should include realistic concurrency, payload sizes, dependency behavior, and error conditions.

Practical Engineering Applications

In a web application, asynchronous I/O is useful when requests spend time waiting for databases or external APIs. A service can issue independent requests concurrently and combine the results, reducing total waiting time. However, it must enforce connection limits, request timeouts, cancellation, and dependency-specific rate limits.

In batch processing, a worker pool can process independent jobs in parallel. The queue should record job state so that failed work can be retried safely. Idempotency is important: repeating a job should not produce duplicate payments, messages, or records. Exactly-once behavior is difficult in distributed systems; designs often aim for at-least-once delivery combined with idempotent consumers.

In front-end applications, asynchronous operations keep the user interface responsive. Long computations should be moved to worker threads or separate processes when the main event loop must remain responsive. Cancellation is essential when a user changes a search query before the previous request completes; otherwise, an older response may overwrite a newer one.

In data-intensive systems, parallelism can accelerate transformations, but shared databases may become the bottleneck. Scaling workers without scaling the database can increase contention, connection exhaustion, and failure rates. Capacity planning must therefore consider the complete dependency chain rather than a single application process.

Illustrative Case Study: Improving an Order-Service API

Background

An illustrative e-commerce company operates an order-service API. The service validates an order, retrieves customer information, checks inventory, calculates shipping, and records the order. Initially, each operation is performed sequentially in a request-handling thread.

Situation and Problem

Under normal load, average response time is acceptable, but the p99 latency rises sharply during promotional events. Users sometimes submit the same order twice because the first request appears to time out. Application logs show increasing thread counts, database connection exhaustion, and repeated calls to the shipping provider.

Analysis

Profiling shows that much of the request time is spent waiting for independent network operations. The service also creates an excessive number of threads, and each thread may hold a database connection while waiting for another dependency. Queue depth and connection utilization rise near saturation. The absence of an idempotency key permits a retry to create a duplicate order.

Approach and Strategy

The team redesigns the request flow using asynchronous I/O for independent external calls. It introduces a bounded concurrency limit for shipping requests and a separate connection pool for database access. Timeouts and cancellation prevent abandoned requests from consuming resources indefinitely. An idempotency key supplied by the client is stored with the order, allowing repeated submissions to return the original result rather than create a new order.

The service adds structured tracing, p50, p95, and p99 latency dashboards, queue-depth alerts, dependency error rates, and load tests representing promotional traffic. The inventory update remains protected by a transaction and a database constraint because concurrent orders must not exceed available stock.

Outcome

The redesign reduces unnecessary waiting and prevents unbounded thread growth. More importantly, it improves reliability: overload is visible, dependency calls are bounded, and retries are safe. The example demonstrates that performance improvement is not merely a matter of adding workers. It requires identifying the bottleneck, controlling concurrency, preserving correctness, and measuring operational outcomes.

Learning Points

  • Independent I/O operations can often be overlapped.
  • Bounded concurrency is safer than unlimited parallel requests.
  • Tail latency and saturation reveal problems hidden by averages.
  • Idempotency and cancellation are essential parts of reliable asynchronous design.
  • Database and external-service capacity constrain application scalability.

Risks, Limitations and Professional Considerations

Concurrency increases cognitive and testing complexity. Timing-dependent bugs may not appear in ordinary tests and may be difficult to reproduce. Excessive locking can eliminate the benefit of parallelism, while inadequate locking can corrupt state. Async code can also suffer from accidental blocking calls, forgotten error handling, unobserved tasks, and incorrect cancellation behavior.

Concurrency policies should respect security and governance requirements. Shared mutable state may expose confidential data across requests if isolation is incorrect. Logs and traces must avoid recording sensitive information while still supporting diagnosis. Resource limits protect availability and can help resist denial-of-service conditions. Access to worker queues, administrative controls, and operational metrics should follow least-privilege principles.

Organizations should document service-level objectives, capacity assumptions, timeout budgets, retry policies, and ownership of operational alerts. Automatic retries must use bounded attempts and backoff; otherwise, a failing dependency can be overwhelmed by a retry storm. Changes to scheduling, queues, or synchronization should receive code review and regression testing.

Current Developments

Contemporary systems increasingly combine asynchronous services, container orchestration, message brokers, serverless functions, and distributed tracing. Structured concurrency is gaining attention because it treats child tasks as a controlled lifetime-bound group: failures and cancellation can be propagated rather than leaving orphaned background work. Observability platforms correlate logs, metrics, and traces to support diagnosis across service boundaries.

Virtual or lightweight threads can make blocking-style code scalable for workloads with many waiting tasks, although they do not remove CPU, database, or external-service limits. WebAssembly, edge computing, and heterogeneous hardware also create new performance choices. In every case, abstractions should be evaluated through workload-specific measurements rather than assumed benefits.

Best Practices for Scalable Concurrent Programs

1. Choose concurrency according to the workload: asynchronous I/O for waiting, parallel computation for CPU-intensive independent work.

2. Prefer immutable data, ownership rules, and message passing over unrestricted shared mutable state.

3. Use bounded queues, worker pools, connection pools, timeouts, and cancellation.

4. Establish lock ordering and keep critical sections short.

5. Make retries bounded, observable, and safe through idempotency.

6. Measure percentiles, throughput, saturation, queue depth, and errors rather than relying only on averages.

7. Profile before optimizing and validate improvements under realistic load.

8. Test race conditions, failure paths, overload, cancellation, and partial dependency failure.

9. Treat operational limits as part of the design, not as deployment details.

10. Document assumptions so future developers can preserve correctness and capacity.

Chapter Summary

Concurrency enables several tasks to make progress during overlapping periods, while parallelism involves simultaneous execution. Processes provide isolation; threads share process memory and therefore require disciplined synchronization. Asynchronous programming, futures, promises, and event loops allow programs to continue useful work while I/O is pending, but blocking operations and CPU-heavy handlers can still damage responsiveness.

Race conditions, deadlocks, starvation, livelocks, priority inversion, and unsafe shared state are major correctness risks. Immutability, message passing, bounded worker pools, lock ordering, timeouts, and cancellation provide practical safeguards. Producer-consumer, fork-join, and pipeline models help structure concurrent work, while Amdahl’s Law explains why serial portions limit speed-up.

Performance decisions should be based on profiling and operational evidence. Latency percentiles, throughput, utilization, saturation, queue depth, errors, and traces reveal both bottlenecks and reliability risks. Professional systems balance speed with correctness, resource limits, security, maintainability, and safe behavior under failure.

Key Terms

Process: An operating-system-managed execution environment with its own address space.

Thread: An execution path within a process that shares process resources.

Concurrency: The ability for multiple tasks to make progress during overlapping periods.

Parallelism: Simultaneous execution of multiple tasks, usually on different cores.

Asynchronous programming: Programming in which a task can continue without waiting synchronously for an operation to finish.

Event loop: A mechanism that dispatches callbacks or continuations when events and I/O operations become ready.

Future or promise: An object representing a result that may become available later.

Critical section: A region of code that accesses shared state and requires controlled execution.

Mutex: A mutual-exclusion lock allowing one holder at a time.

Semaphore: A synchronization object that controls access through a limited number of permits.

Race condition: Behavior whose outcome depends on the timing or ordering of concurrent operations.

Deadlock: Indefinite waiting caused by tasks holding resources while waiting for one another.

Starvation: Indefinite or excessive waiting because a task is repeatedly denied resources or scheduling.

Livelock: Repeated activity in which tasks fail to make useful progress.

Thread safety: Correct behavior of a component under its documented concurrent use.

Backpressure: Flow control that prevents producers from overwhelming consumers.

Latency: Time taken to complete an operation.

Throughput: Amount of work completed per unit of time.

Saturation: A condition in which demand approaches or exceeds resource capacity.

Idempotency: A property whereby repeating an operation has the same intended effect as performing it once.

Review Questions

Short Answer Questions

1. Distinguish concurrency from parallelism.

2. What role does an event loop play in asynchronous programming?

3. Define a race condition and explain why it is difficult to reproduce.

4. What is the purpose of a semaphore?

5. Why are latency percentiles often more useful than average latency?

Long Answer Questions

1. Explain the differences between processes and threads, including their benefits and risks.

2. Describe futures, promises, callbacks, and async/await as approaches to asynchronous programming.

3. Explain the four conditions associated with deadlock and discuss prevention strategies.

4. Compare producer-consumer, fork-join, and pipeline concurrency models.

5. Develop a systematic methodology for profiling and improving the performance of a concurrent service.

Analytical / Application Questions

1. A service increases its worker-thread count but experiences higher p99 latency and database connection failures. Analyze the likely causes and recommend a revised design.

2. Two threads update a shared inventory counter, and occasional overselling occurs. Explain the concurrency fault and propose at least two correct solutions.

3. An asynchronous application becomes unresponsive whenever it processes large files. Diagnose the event-loop problem and design an improved execution strategy.

22. Multiple Choice Questions with Answers

Question 1

Which statement best distinguishes concurrency from parallelism?

A. Concurrency always requires multiple processor cores.

B. Parallelism means tasks overlap logically but never execute simultaneously.

C. Concurrency allows overlapping progress, whereas parallelism involves simultaneous execution.

D. Concurrency applies only to distributed systems.

Correct Answer: C. Concurrency allows overlapping progress, whereas parallelism involves simultaneous execution.

Question 2

Why can an asynchronous event-loop application become unresponsive?

A. Because futures always create deadlocks.

B. Because a long CPU-bound or blocking operation occupies the event-loop thread.

C. Because asynchronous programs cannot perform network I/O.

D. Because event loops require every task to use a separate process.

Correct Answer: B. Because a long CPU-bound or blocking operation occupies the event-loop thread.

Question 3

Two threads read the same counter value, increment it, and both write the result. What is the primary problem?

A. Priority inversion

B. Lost update caused by a race condition

C. Backpressure

D. Memory isolation

Correct Answer: B. Lost update caused by a race condition

Question 4

Which design choice most directly reduces the possibility of circular-wait deadlocks?

A. Creating unlimited threads

B. Acquiring locks according to a consistent global order

C. Increasing queue capacity without limit

D. Removing all timeouts

Correct Answer: B. Acquiring locks according to a consistent global order

Question 5

A queue grows continuously because producers generate work faster than consumers process it. Which control is most appropriate?

A. Unboundedly increase the queue size

B. Use backpressure and a bounded queue

C. Remove monitoring of queue depth

D. Add retries for every queued item

Correct Answer: B. Use backpressure and a bounded queue

Question 6

Which workload is most likely to benefit from asynchronous I/O?

A. A calculation that continuously uses all CPU cores

B. A request that waits on several independent network services

C. A single-threaded sorting algorithm with no waiting

D. A cryptographic computation with no external dependencies

Correct Answer: B. A request that waits on several independent network services

Question 7

Why can p99 latency reveal a problem that average latency hides?

A. It measures only successful database writes.

B. It describes the slowest portion of requests and exposes tail delays.

C. It is always lower than average latency.

D. It measures processor temperature rather than response time.

Correct Answer: B. It describes the slowest portion of requests and exposes tail delays.

Question 8

Which practice best supports safe retrying of an order-submission request?

A. Removing transaction constraints

B. Using an idempotency key

C. Increasing the number of retries without limit

D. Ignoring duplicate responses

Correct Answer: B. Using an idempotency key

Question 9

According to the practical implication of Amdahl’s Law, adding processors will eventually provide limited benefit when:

A. All work is perfectly independent

B. The program contains a significant sequential portion

C. The system uses immutable objects

D. The workload contains network I/O

Correct Answer: B. The program contains a significant sequential portion

Question 10

Which statement best describes thread safety?

A. A component always executes faster when more threads are added.

B. A component behaves correctly under its documented concurrent usage.

C. A component never uses synchronization.

D. A component must create one process per request.

Correct Answer: B. A component behaves correctly under its documented concurrent usage.

Programming Fundamentals and Object-Oriented Programming
Chapter 14 of 15