A Guide To Taming Blocking Operations With Project Reactor
In this article, we will explore how to effectively handle situations where blocking operations are necessary within the context of Spring WebFlux.
Join the DZone community and get the full member experience.
Join For FreeIn the world of modern web application development, responsiveness and scalability are paramount. Users expect lightning-fast responses and seamless interactions, and to meet these expectations, developers have embraced reactive programming paradigms. Spring WebFlux, with its non-blocking, asynchronous foundation, is a cornerstone of this revolution.
However, even in the realm of reactive programming, there are often unavoidable encounters with the blocking world. Whether it's interfacing with legacy systems, accessing a traditional database, or integrating with third-party libraries that haven't made the leap to reactive yet, developers find themselves at the crossroads of reactivity and blocking operations.
Understanding Blocking and Non-Blocking Operations
The terms "blocking" and "non-blocking" operations are fundamental concepts that significantly impact the performance and responsiveness of applications. Before we dive into strategies for handling blocking operations in Spring WebFlux, it's crucial to establish a clear understanding of what these terms mean and how they differ.
Blocking Operations
Blocking operations, as the name suggests, are operations that block or halt the execution of the program until a specific task is completed. When a blocking operation is invoked, the program waits for the operation to finish before proceeding to the next instruction. This type of operation is often associated with synchronous code execution.
Consider, for example, a traditional database query. When your application makes a synchronous database call, it waits for the database to process the request and return the result. During this time, no other work can be done by your application, causing potential bottlenecks and reduced responsiveness.
Non-Blocking Operations
Conversely, non-blocking operations are designed to avoid such waiting. In a non-blocking operation, when a request is made, the program continues executing other tasks without waiting for the operation to complete. It initiates the operation and then checks periodically for its completion while allowing other work to continue.
Non-blocking operations are the cornerstone of reactive programming and are synonymous with asynchronous programming. In a reactive system, work is organized around the principle of responsiveness, ensuring that the application can handle a large number of concurrent tasks without becoming unresponsive.
Examples of non-blocking operations include asynchronous HTTP requests, event-driven programming, and reactive streams, where tasks are initiated and then processed as they complete, allowing the program to remain highly responsive.
Key Distinctions:
- Blocking operations can lead to thread contention and resource wastage when multiple threads are waiting for resources.
- Non-blocking operations maximize resource utilization and enable more efficient handling of multiple concurrent tasks.
- Reactive frameworks like Spring WebFlux leverage non-blocking operations to provide high concurrency and responsiveness.
In this article, we will explore how to effectively handle situations where blocking operations are necessary within the context of Spring WebFlux, a reactive programming framework designed to excel in non-blocking scenarios.
We will commence with an illustrative code example that incorporates both blocking and non-blocking elements.
package blockingvsnonblocking;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
@Slf4j
public class BlockingVsNonBlocking {
public static String readData(String filePath) throws InterruptedException {
log.info("Started reading data from " + filePath);
Thread.sleep(3000);
log.info("Completed reading data from " + filePath);
return String.format("Data from %s", filePath);
}
public static void main(String[] args) throws Exception {
String firstFilePath = "a/file/path/to/read";
String secondFilePath = "another/file/path/to/read";
blocking(firstFilePath, secondFilePath);
nonBlocking(firstFilePath, secondFilePath);
}
private static void nonBlocking(String firstFilePath, String secondFilePath) throws InterruptedException {
Mono<String> data = makeNonBlocking(() -> readData(firstFilePath));
Mono<String> data2 = makeNonBlocking(() -> readData(secondFilePath));
CountDownLatch latch = new CountDownLatch(1);
data.zipWith(data2)
.doOnTerminate(latch::countDown)
.subscribe(tup->{
log.info("Async Read data: " +tup.getT1());
log.info("Async Read data: " +tup.getT2());
});
latch.await();
}
static <T> Mono<T> makeNonBlocking(Callable<T> callable){
return Mono.fromCallable(callable).subscribeOn(Schedulers.boundedElastic());
}
private static void blocking(String firstFilePath, String secondFilePath) throws InterruptedException {
String data = readData(firstFilePath);
String data2 = readData(secondFilePath);
log.info("Read data: " +data);
log.info("Read data: " +data2);
}
}
In This Code:
Blocking Operation:
- The
blocking
method is called withfirstFilePath
andsecondFilePath
as arguments. This method performs thereadData
operation sequentially, one after the other. - Inside the
blocking
method,readData
is invoked, which simulates a blocking operation by causing the current thread to sleep for 3 seconds. During this sleep period, the thread is blocked, and no other tasks can execute. - After the
readData
calls, the data read from both files is logged as "Read data."
Non-Blocking Operation:
- The
nonBlocking
method is called with the same file paths as arguments. - Inside the
nonBlocking
method, twoMono
instances (data
anddata2
) are created using themakeNonBlocking
method. TheseMono
instances represent asynchronous operations. - The
zipWith
operator is used to combine the results of the twoMono
instances. This allows you to process the results when both operations are complete. - The
subscribe
method is called on the combinedMono
, and it logs the data read from both files as "Async Read data." - A
CountDownLatch
namedlatch
is used to ensure that the program doesn't exit until the asynchronous operations have completed.latch.await()
blocks the main thread until theCountDownLatch
reaches zero, which is decremented in thedoOnTerminate
callback.
This code clearly illustrates the contrast between blocking and non-blocking operations. The blocking operation (blocking
) executes tasks sequentially, while the non-blocking operation (nonBlocking
) allows concurrent execution of asynchronous tasks, making it more suitable for scenarios where responsiveness and concurrency are essential.
Published at DZone with permission of Dursun Koç, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments