docs: improve half-sync/half-async

This commit is contained in:
Ilkka Seppälä
2024-04-24 21:45:00 +03:00
parent c0e20ec570
commit ad3aef7b71
3 changed files with 108 additions and 126 deletions
+100 -118
View File
@@ -3,160 +3,142 @@ title: Half-Sync/Half-Async
category: Concurrency
language: en
tag:
- Performance
- Asynchronous
- Decoupling
- Synchronization
- Thread Management
---
## Also known as
* Async-Sync Bridge
* Half-Synchronous/Half-Asynchronous
## Intent
The Half-Sync/Half-Async pattern decouples synchronous I/O from
asynchronous I/O in a system to simplify concurrent programming effort without
degrading execution efficiency.
The Half-Sync/Half-Async pattern aims to decouple asynchronous and synchronous processing in concurrent systems, allowing efficient interaction and data exchange between asynchronous and synchronous components.
## Class diagram
![Half-Sync/Half-Async class diagram](./etc/half-sync-half-async.png)
## Applicability
Use Half-Sync/Half-Async pattern when
* a system possesses following characteristics:
* the system must perform tasks in response to external events that occur asynchronously, like hardware interrupts in OS
* it is inefficient to dedicate separate thread of control to perform synchronous I/O for each external source of event
* the higher level tasks in the system can be simplified significantly if I/O is performed synchronously.
* one or more tasks in a system must run in a single thread of control, while other tasks may benefit from multi-threading.
## Explanation
The Half-Sync/Half-Async pattern is a concurrency design pattern used in software engineering to manage concurrent operations and interactions in a system. It's particularly useful in applications where both asynchronous and synchronous processing are required to achieve optimal performance and responsiveness.
The pattern is structured into two components: the synchronous part (Half-Sync) and the asynchronous part (Half-Async).
Real world example
Half-Sync (Synchronous Part):
In this part, there is a synchronous layer that handles high-level control flow and coordination of the application. It typically consists of a set of threads that handle communication, synchronization, and coordination. These threads are responsible for managing shared resources and orchestrating the flow of work in a synchronous manner.
> Imagine a busy restaurant kitchen where the process of taking orders is asynchronous, allowing waiters to continue taking orders from customers without waiting for the chefs to cook the previous ones. Meanwhile, the cooking (synchronous part) follows a specific sequence and requires waiting for each dish to be prepared before starting the next. This setup enables the restaurant to handle multiple customer orders efficiently, while ensuring each dish is cooked with the required attention and timing, much like the Half-Sync/Half-Async pattern manages asynchronous tasks and synchronous processing in software systems.
Half-Async (Asynchronous Part):
The asynchronous part involves a set of asynchronous workers or threads that perform the actual processing and execution of tasks. These threads are responsible for carrying out time-consuming or potentially blocking operations asynchronously. They handle I/O operations, long-running computations, or any task that can be parallelized effectively.
In plain words
How it Works:
> The Half-Sync/Half-Async pattern separates operations into asynchronous tasks that handle events without waiting, and synchronous tasks that process these events in an orderly and blocking manner.
Synchronous Part:
Wikipedia says
The synchronous part handles incoming requests, organizes the tasks to be performed, and dispatches them to the asynchronous workers.
It's responsible for high-level orchestration, resource management, and synchronization of the overall system.
Asynchronous Part:
Asynchronous workers execute the tasks independently and concurrently without blocking the main application.
They handle time-consuming or I/O-bound tasks efficiently, ensuring the system remains responsive.
Advantages:
Responsiveness: Asynchronous processing ensures that the system remains responsive by allowing non-blocking operations.
Parallel Execution: It enables concurrent execution of tasks, improving performance by utilizing multiple threads or processes.
Scalability: The asynchronous part can be scaled easily to handle a larger number of requests efficiently.
Disadvantages:
Complexity: Implementing and managing the two parts can be complex, requiring careful coordination and synchronization mechanisms.
Potential Deadlocks: The interplay between the synchronous and asynchronous parts can introduce potential deadlocks or race conditions if not handled correctly.
The Half-Sync/Half-Async pattern strikes a balance between responsiveness and efficiency by utilizing both synchronous and asynchronous processing to optimize the performance of a system while ensuring responsiveness to user requests.
> The Half-Sync/Half-Async design pattern is used to solve situations where one part of the application runs synchronously while another runs asynchronously, and the two modules need to communicate with each other.
## Programmatic Example
The Half-Sync/Half-Async design pattern is a concurrency pattern that separates synchronous and asynchronous processing in a system, simplifying the programming model without affecting performance. It's particularly useful in scenarios where you have a mix of short, mid, and long duration tasks.
In the provided code, we can see an example of the Half-Sync/Half-Async pattern in the `App`, `AsynchronousService`, and `ArithmeticSumTask` classes.
The `App` class is the entry point of the application. It creates an instance of `AsynchronousService` and uses it to handle various tasks asynchronously.
```java
{
import java.util.LinkedList;
import java.util.Queue;
public class App {
class TaskQueue {
private Queue<String> queue = new LinkedList<>();
synchronized void enqueue(String task) {
queue.add(task);
notify(); // Notify waiting threads that a task is available
}
synchronized String dequeue() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // Wait until a task is enqueued
}
return queue.poll();
}
public static void main(String[] args) {
var service = new AsynchronousService(new LinkedBlockingQueue<>());
service.execute(new ArithmeticSumTask(1000));
service.execute(new ArithmeticSumTask(500));
service.execute(new ArithmeticSumTask(2000));
service.execute(new ArithmeticSumTask(1));
service.close();
}
}
```
class SynchronousPart extends Thread {
private TaskQueue taskQueue;
The `AsynchronousService` class is the asynchronous part of the system. It manages a queue of tasks and processes them in a separate thread.
public SynchronousPart(TaskQueue taskQueue) {
this.taskQueue = taskQueue;
}
@Override
public void run() {
while (true) {
try {
String task = taskQueue.dequeue();
System.out.println("Synchronous part processing task: " + task);
// Simulate some synchronous processing
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
```java
public class AsynchronousService {
// Implementation details...
}
```
class AsynchronousPart extends Thread {
@Override
public void run() {
while (true) {
// Simulate some asynchronous processing
System.out.println("Asynchronous part processing tasks...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
The `ArithmeticSumTask` class represents a task that can be processed asynchronously. It implements the `AsyncTask` interface, which defines methods for pre-processing, post-processing, and error handling.
```java
static class ArithmeticSumTask implements AsyncTask<Long> {
private final long numberOfElements;
public ArithmeticSumTask(long numberOfElements) {
this.numberOfElements = numberOfElements;
}
@Override
public Long call() throws Exception {
return ap(numberOfElements);
}
@Override
public void onPreCall() {
if (numberOfElements < 0) {
throw new IllegalArgumentException("n is less than 0");
}
}
@Override
public void onPostCall(Long result) {
LOGGER.info(result.toString());
}
@Override
public void onError(Throwable throwable) {
throw new IllegalStateException("Should not occur");
}
}
```
public class HalfSyncHalfAsyncExample {
public static void main(String[] args) {
TaskQueue taskQueue = new TaskQueue();
In this example, the `App` class enqueues tasks to the `AsynchronousService`, which processes them asynchronously. The `ArithmeticSumTask` class defines the task to be processed, including pre-processing, the actual processing, and post-processing steps. This is a basic example of the Half-Sync/Half-Async pattern, where tasks are enqueued and processed asynchronously, while the main thread continues to handle other tasks.
SynchronousPart synchronousThread = new SynchronousPart(taskQueue);
AsynchronousPart asynchronousThread = new AsynchronousPart();
## Applicability
synchronousThread.start();
asynchronousThread.start();
Use the Half-Sync/Half-Async pattern in scenarios where:
// Enqueue tasks
taskQueue.enqueue("Task 1");
taskQueue.enqueue("Task 2");
taskQueue.enqueue("Task 3");
// Allow the threads to run for a while
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Stop the threads (in a real application, you would have a proper way to signal the threads to stop)
synchronousThread.interrupt();
asynchronousThread.interrupt();
}
}
```
### In this Java example:
* TaskQueue class manages the task queue and provides methods for enqueueing and dequeuing tasks in a thread-safe manner.
* SynchronousPart and AsynchronousPart classes represent the synchronous and asynchronous parts of the application, respectively.
* The synchronous part processes tasks in a synchronous manner, and the asynchronous part processes tasks in an asynchronous manner, simulating some processing time.
* The main program creates instances of TaskQueue, SynchronousPart, and AsynchronousPart, enqueues tasks, and starts the threads to demonstrate the Half-Sync/Half-Async pattern.
* High-performance is required and the system must handle asynchronous operations along with synchronous processing.
* The system needs to effectively utilize multicore architectures to balance tasks between asynchronous and synchronous processing.
* Decoupling of asynchronous tasks from synchronous processing is necessary to simplify the design and implementation.
## Real world examples
## Known uses
* [BSD Unix networking subsystem](https://www.dre.vanderbilt.edu/~schmidt/PDF/PLoP-95.pdf)
* [Real Time CORBA](http://www.omg.org/news/meetings/workshops/presentations/realtime2001/4-3_Pyarali_thread-pool.pdf)
* [Android AsyncTask framework](https://developer.android.com/reference/android/os/AsyncTask)
* Java's standard libraries utilize this pattern with thread pools and execution queues in the concurrency utilities (e.g., java.util.concurrent).
* Network servers handling concurrent connections where IO operations are handled asynchronously and processing of requests is done synchronously.
## Consequences
Benefits:
* Improves responsiveness and throughput by separating blocking operations from non-blocking operations.
* Simplifies programming model by isolating asynchronous and synchronous processing layers.
Trade-offs:
* Adds complexity in managing two different processing modes.
* Requires careful design to avoid bottlenecks between the synchronous and asynchronous parts.
## Related Patterns
* [Leader/Followers](https://java-design-patterns.com/patterns/leader-followers/): Both patterns manage thread assignments and concurrency, but Leader/Followers uses a single thread to handle all I/O events, dispatching work to others.
* [Producer/Consumer](https://java-design-patterns.com/patterns/producer-consumer/): Can be integrated with Half-Sync/Half-Async to manage work queues between the async and sync parts.
* [Reactor](https://java-design-patterns.com/patterns/reactor/): Often used with Half-Sync/Half-Async to handle multiple service requests delivered to a service handler without blocking the handler.
## Credits
* [Douglas C. Schmidt and Charles D. Cranor - Half Sync/Half Async](https://www.dre.vanderbilt.edu/~schmidt/PDF/PLoP-95.pdf)
* [Pattern Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects](https://www.amazon.com/gp/product/0471606952/ref=as_li_tl?ie=UTF8&camp=1789&creative=9325&creativeASIN=0471606952&linkCode=as2&tag=javadesignpat-20&linkId=889e4af72dca8261129bf14935e0f8dc)
* [Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects](https://amzn.to/3UgC24V)
* [Java Concurrency in Practice](https://amzn.to/4aRMruW)
@@ -32,7 +32,7 @@ import lombok.extern.slf4j.Slf4j;
* AsyncTask} and {@link AsynchronousService}.
*
* <p><i>PROBLEM</i> <br>
* A concurrent system have a mixture of short duration, mid duration and long duration tasks. Mid
* A concurrent system have a mixture of short duration, mid-duration and long duration tasks. Mid
* or long duration tasks should be performed asynchronously to meet quality of service
* requirements.
*
@@ -47,7 +47,7 @@ import lombok.extern.slf4j.Slf4j;
* associated with each socket that is connected to the client. Thread blocks waiting for CORBA
* requests from the client. On receiving request it is inserted in the queuing layer which is then
* picked up by synchronous layer which processes the request and sends response back to the
* client.<br> Android AsyncTask framework - Framework provides a way to execute long running
* client.<br> Android AsyncTask framework - Framework provides a way to execute long-running
* blocking calls, such as downloading a file, in background threads so that the UI thread remains
* free to respond to user inputs.<br>
*
@@ -101,18 +101,18 @@ public class App {
}
/*
* This is the long running task that is performed in background. In our example the long
* running task is calculating arithmetic sum with artificial delay.
* This is the long-running task that is performed in background. In our example the long-running
* task is calculating arithmetic sum with artificial delay.
*/
@Override
public Long call() throws Exception {
public Long call() {
return ap(numberOfElements);
}
/*
* This will be called in context of the main thread where some validations can be done
* regarding the inputs. Such as it must be greater than 0. It's a small computation which can
* be performed in main thread. If we did validated the input in background thread then we pay
* be performed in main thread. If we did validate the input in background thread then we pay
* the cost of context switching which is much more than validating it in main thread.
*/
@Override
@@ -43,7 +43,7 @@ import lombok.extern.slf4j.Slf4j;
public class AsynchronousService {
/*
* This represents the queuing layer as well as synchronous layer of the pattern. The thread pool
* contains worker threads which execute the tasks in blocking/synchronous manner. Long running
* contains worker threads which execute the tasks in blocking/synchronous manner. Long-running
* tasks should be performed in the background which does not affect the performance of main
* thread.
*/
@@ -79,7 +79,7 @@ public class AsynchronousService {
return;
}
service.submit(new FutureTask<T>(task) {
service.submit(new FutureTask<>(task) {
@Override
protected void done() {
super.done();