- Synchronous Service Layer: deals with long-running tasks, implements the core business logic
- Queuing Layer: a request queue, doesn't block the caller
- Asynchronous Service Layer: dispatches incoming requests to the queue
There might be a number of concurrently running synchronous services. The queueing layer is responsible for thread synchronization.
Advantages
- Reduced code complexity: Synchronized services focus solely on the core logic.
- Separation of concerns: Each of the layers is relatively self-contained and serves a different purpose.
- Centralized communication: The queuing layer mediates all communication - no moving parts flying around
Drawbacks
- Performance overhead: "Boundary-crossing penalty" - context switching, synchronization, data transfer ..
- Harder to debug: Asynchronous callbacks make testing and debugging less straightforward
- Benefit questionable: Higher-level application services may not benefit from asynchronous I/O. That depends on framework / OS design.
Example
An ASCII Art generator (credit goes to Evilzone) is not only pleasant to work with, but it is also a suitable candidate for a long-running task. I saw it as a perfect fit for the pattern.
public class AsciiArtGenerator {
..
/**
* Converts an image to its ASCII representation.
*
* @param imgPath path to the image, relative to /src/main/resources
* @param outPath path to the resulting text file, relative to ./data
* @return true, if the conversion succeeds, false otherwise
*/
public boolean convertToAscii(String imgPath, String outPath) {..}
}
The image-to-text conversion is a synchronous blocking task, which might take a while to complete. As such it's bound to run in a background thread.
The front-end of the app is served asynchronously via a non-blocking dispatcher:
/**
* Represents an asynchronous layer, as it forwards
* client requests for further processing and returns
* immediately. It receives results via notifications.
*
* @author: Tomas Zezula
* Date: 24/08/2014
*/
public class NonBlockingDispatcher {
..
/**
* Sends a request to the queue and returns instantly.
*
* @param imgPath Image path for the ASCII generator
* @param outPath Output path for the ASCII generator
*/
public void dispatch(final String imgPath, final String outPath) {..}
/**
* Captures processing result and notifies the subscribed client
*
* @param result true, if success, false otherwise
*/
public void onResult(boolean result) {..}
}
Finally, the communication between the dispatcher and the worker thread is mediated by a dedicated queuing channel:
/**
* Queues incoming requests and notifies the dispatcher when the response is ready.
*
* @author: Tomas Zezula
* Date: 24/08/2014
*/
public class WorkQueue {..}
As usual, the example is accompanied by unit tests proving the core concepts. This time, I kept the tests to a bare minimum, just to highlight the major difference between a naive single-threaded synchronous approach and the slightly more advanced asynchronous implementation.
The app pays tribute to a great actress of the last century. The resulting file (./data/audrey.txt) is best viewed using a minimal font-size.
Source Code
Resources