Smooth Concurrency: A Practical Guide to Non-Blocking Delays Across Platforms

Smart AI Code2026-10-017 min read

In software development, we often need to wait before executing an action: retrying failed network requests, periodically refreshing data, or controlling hardware blinking. Many beginners use blocking delay functions (Thread.sleep, delay(), etc.) that freeze the current thread or task, preventing other events from being processed and causing unresponsive programs. Non-blocking delays allow the program to continue executing other tasks during the wait, maintaining smoothness and responsiveness.

This article provides practical code examples across seven platforms—JavaScript, Java, Kotlin, Android Compose, embedded (Arduino/ESP32/STM32), VBA, and Linux—to help you master the core usage of non-blocking delays.

1. Blocking vs. Non-Blocking: A World of Difference

Blocking delays are like waiting on a phone call for the other party to pick up—you can do nothing else during that time. Non-blocking delays are like hanging up and doing other things, then processing the call when the other party returns. In programming, blocking means the thread or task is frozen; non-blocking means the delay task is handed to the system scheduler, and the current thread immediately returns to handle other work.

Typical Problems with Blocking Delays

  • Resource waste: Threads sit idle while waiting, yet occupy valuable system resources.
  • Poor responsiveness: UI thread blocking causes the interface to freeze; sensor data cannot be read on microcontrollers.
  • Poor scalability: A large number of threads blocked on the server side prevent handling new requests.

Advantages of Non-Blocking Delays

  • High concurrency: Threads or coroutines are released during waits, allowing more tasks to be processed.
  • Smooth user experience: The UI thread never stalls, keeping animations and interactions fluid.
  • Hardware efficiency: The CPU executes other logic during the delay instead of spinning in a loop.

2. Practical Non-Blocking Delay Examples Across Platforms

2.1 JavaScript​: setTimeout and async/await

JavaScript is single-threaded; a blocking "busy-wait" loop would freeze the entire page. Non-blocking delays are fundamental to JavaScript development.

Callback form: setTimeout

setTimeout accepts a callback function and a delay in milliseconds; the callback executes after the specified time without blocking the code execution.

javascript console.log("Executing now");

setTimeout(() => { console.log("Executing after 1 second"); }, 1000);

console.log("This line executes immediately, without waiting");

Output order:

Executing now This line executes immediately, without waiting Executing after 1 second

async/await form: delay helper function

If you prefer async/await chaining, you can use an async/await pattern with a delay function (built on Promise + setTimeout).

javascript const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function example() { console.log("Start"); await delay(1000); console.log("Continuing after 1 second"); }

example();

Note: JavaScript has no built-in sleep function. The so-called "sleep loop" (e.g., while (Date.now() - start < 1000) {}) completely blocks the main thread and makes the page unresponsive; it should be strictly avoided.

Source: alvarotrigo.com

2.2 Java: CompletableFuture.delayedExecutor

Java 9 and above provide CompletableFuture.delayedExecutor, which returns a special Executor that executes submitted tasks after a specified delay, rather than immediately.

Basic usage

java import java.util.concurrent.*;

public class DelayedExecutorExample { public static void main(String[] args) { long start = System.currentTimeMillis(); System.out.println("[" + elapsed(start) + "ms] Preparing to submit delayed task");

    Executor delayed = CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS);

    CompletableFuture.runAsync(() -> {
        System.out.println("[" + elapsed(start) + "ms] Task executed! Thread: "
            + Thread.currentThread().getName());
    }, delayed);

    System.out.println("[" + elapsed(start) + "ms] Main thread continues, not blocked");

    try { Thread.sleep(4000); } catch (InterruptedException e) {}
}

static long elapsed(long start) {
    return System.currentTimeMillis() - start;
}

}

Output:

[0ms] Preparing to submit delayed task [0ms] Main thread continues, not blocked [2001ms] Task executed! Thread name: ForkJoinPool.commonPool-worker-1

Specifying a custom thread pool

The three-argument overload of delayedExecutor allows you to specify which thread pool the delayed task will eventually run in:

java ExecutorService myPool = Executors.newFixedThreadPool(2, r -> { Thread t = new Thread(r); t.setName("my-pool-" + t.getId()); return t; });

CompletableFuture.supplyAsync(() -> { System.out.println("Task executed! Thread: " + Thread.currentThread().getName()); return "result"; }, CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS, myPool));

Practical scenario: non-blocking retry

When a microservice call fails, you typically need to wait a while before retrying. Using delayedExecutor avoids occupying a thread pool during the retry wait:

java CompletableFuture.runAsync(() -> {}, CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS)) .whenComplete((v, ex) -> { pool.submit(() -> retryTask()); });

Note: delayedExecutor is suitable only for one-off tasks with relative delays. For tasks that run at a fixed time every day or periodically, use ScheduledExecutorService instead.

Source: cnblogs.com

2.3 Kotlin and Android: Coroutines' delay() and LaunchedEffect

Kotlin coroutines are a lightweight concurrency solution in Android development. The delay() function is a non-blocking suspend function—it suspends the current coroutine and releases the underlying thread, rather than freezing the thread.

Using delay() in coroutines

kotlin import kotlinx.coroutines.*

fun main() = runBlocking { launch { delay(1000L) println("Kotlin coroutine finished!") } println("Main coroutine continues...") }

LaunchedEffect in Jetpack Compose

In Jetpack Compose, LaunchedEffect is used to execute asynchronous code after the UI has rendered. Its key characteristic: it does not block UI rendering. Compose's UI drawing is synchronous; code inside LaunchedEffect is deferred until the main thread is idle.

kotlin @Composable fun MyScreen() { LaunchedEffect(Unit) { delay(500) println("Delayed execution, UI scrolling remains completely smooth") } }

Coroutines vs. threads: Each coroutine consumes about 2KB of memory, while a traditional thread occupies at least 1MB of stack space. You can easily create thousands of coroutines without exhausting memory.

Source: jianshu.com, CSDN

2.4 Embedded Systems: Non-Blocking Delays from Arduino to STM32

In embedded development, blocking delays (like delay()) cause the CPU to stall, preventing simultaneous task handling. Non-blocking delays are key to multitasking programming.

Arduino: using millis() instead of delay()

millis() returns the number of milliseconds since program startup. Using it allows you to perform timed operations without blocking.

cpp const int LED_PIN = 2; unsigned long previousMillis = 0; const long interval = 1000;

void setup() { pinMode(LED_PIN, OUTPUT); }

void loop() { unsigned long currentMillis = millis();

if (currentMillis - previousMillis >= interval) {
    previousMillis = currentMillis;
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
}

// Continue reading sensors, handling communication, etc.
readSensors();    // illustrative — replace with actual code
updateDisplay();  // illustrative — replace with actual code

}

ESP32: FreeRTOS vTaskDelay()

In the FreeRTOS environment on ESP32, vTaskDelay() suspends the current task and releases the CPU for other tasks to run, achieving a non-blocking delay.

cpp #include "freertos/FreeRTOS.h" #include "freertos/task.h"

void delay_ms(int ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }

void app_main() { while (1) { LedOn(); // illustrative — replace with actual GPIO code vTaskDelay(100 / portTICK_PERIOD_MS); LedOff(); // illustrative — replace with actual GPIO code vTaskDelay(100 / portTICK_PERIOD_MS); } }

STM32: using timer interrupts instead of HAL_Delay()

HAL_Delay() is based on the SysTick timer and waits by looping until the count reaches a specified value, which blocks the CPU. A better approach is to use a hardware timer interrupt: configure the timer to trigger an interrupt when the set time is reached, and the CPU continues executing the main program while waiting.

c // Simplified conceptual example: configure a timer to trigger an interrupt after 1 second // In the interrupt service routine, set a flag; the main program checks the flag and executes accordingly void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { delay_complete = 1; // Set non-blocking delay completion flag TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }

Source: CSDN, wiki.lckfb.com, community.st.com

2.5 Desktop and Office Automation: VBA's Application.OnTime

In Excel or Office VBA, Application.Wait freezes the entire Office application. Using Application.OnTime allows you to schedule a procedure to run at a specified time non-blockingly.

vba ' Non-blocking incremental counter Sub NonBlockingChange() Update Application.OnTime Now + TimeValue("0:00:01"), "NonBlockingChange" End Sub

Sub Update() Range("A1").Value2 = CInt(Range("A1").Value2) + 1 End Sub

This is very similar to JavaScript's setTimeout. To stop it, call Application.OnTime with the fourth argument set to False.

Source: steffriff.co.uk

2.6 Linux User-Space: nanosleep for High-Precision Delays

Under Linux, sleep() and usleep() provide second- and microsecond-level precision, respectively, but usleep() is deprecated. nanosleep() is recommended, providing nanosecond-level precision without busy-waiting (consuming no CPU resources).

c #include <time.h> #include <errno.h>

struct timespec ts = {1, 500000000}; // 1 second + 500 milliseconds nanosleep(&ts, NULL);

If you are concerned about interruption by a signal, check the remaining time and call again:

c struct timespec ts = {2, 0}; struct timespec remaining; while (nanosleep(&ts, &remaining) == -1 && errno == EINTR) { ts = remaining; }

Source: hzjcp.com

3. How to Choose the Right Non-Blocking Delay Solution?

RequirementRecommended Solution
Front-end / Node.js timingsetTimeout or async/await + delay()
Java backend delayed tasksCompletableFuture.delayedExecutor (suitable for one-off relative delays)
Android UI delaysLaunchedEffect + delay() (coroutines)
Embedded multitaskingArduino: millis(); ESP32: vTaskDelay(); STM32: timer interrupts
Office automationVBA Application.OnTime
Linux high-precision delaysnanosleep()
Periodic / absolute-time tasksScheduledExecutorService (Java), setInterval (JS), etc.

Caveats:

  • Non-blocking delays are usually not absolutely precise; actual execution time may vary with system load.
  • Some environments (e.g., JavaScript setTimeout) may throttle in background tabs.
  • In embedded systems, timer interrupt configuration must be combined with the hardware manual to avoid conflicts with other peripherals.

4. Conclusion and Next Steps

Non-blocking delays are an important means of improving application responsiveness and concurrency in modern programming. This article has opened the door for you; each platform has deeper mechanisms and best practices waiting to be explored. We recommend immediately trying a code snippet relevant to your current project and observing whether the smoothness and resource utilization of your program improve after introducing non-blocking delays.

For further learning, consult the official documentation or community discussions for each platform.