Concurrency patterns – Half-Sync / Half-Async
The pattern separates asynchronous I/O from the synchronous one. Main thread doesn’t block on incoming client requests and long-running operations are offloaded to a dedicated synchronous layer. Processing results are delivered by the means of callbacks.
Application
- largely used in operating systems (hardware interrupts, application management)
- Android – AsyncTask (file downloads ..)
A decent queuing system is required to handle messaging between the two layers. The challenge lies in preventing race conditions and other concurrency related issues.
Key Components
-
- 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
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
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 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