流畅并发:多平台非阻塞延时实战指南

Smart AI Code2026-10-01阅读约 10 分钟

在软件开发中,我们经常需要让程序"等一会儿"再执行某个操作:重试失败的网络请求、定时刷新数据、控制硬件闪烁。许多初学者使用的延时函数(Thread.sleep、delay() 等)是阻塞式的——强制当前线程或任务暂停,期间无法处理其他事件,导致程序卡顿、响应不及时。非阻塞延时让程序在等待期间继续执行其他任务,保持流畅与响应性。

本文通过 JavaScript、Java、Kotlin、Android Compose、嵌入式(Arduino / ESP32 / STM32)、VBA、Linux 七个平台的实战代码,带你掌握非阻塞延时的核心用法。

1. 阻塞与非阻塞:一字之差,天壤之别

阻塞式延时像在电话里等对方接通,期间你什么都做不了;非阻塞延时则像挂断电话后去做别的事,等对方回电时再处理。在编程中,阻塞意味着线程或任务被冻结;非阻塞则意味着将延时任务交给系统调度器,当前线程立即返回,继续处理其他工作。

阻塞式延时的典型问题

  • 资源浪费:线程在等待期间闲置,却占用着宝贵的系统资源。
  • 响应迟钝:UI 线程阻塞导致界面冻结;单片机中传感器数据无法读取。
  • 可扩展性差:服务器端大量线程被阻塞,无法处理新请求。

非阻塞延时的优势

  • 高并发:线程或协程在等待期间被释放,可处理更多任务。
  • 流畅的用户体验:UI 线程不卡顿,动画和交互保持流畅。
  • 硬件效率:CPU 在延时期间执行其他逻辑,而非空转。

2. 多平台非阻塞延时实战

2.1 JavaScript:setTimeout 与 async/await

JavaScript 是单线程的,阻塞式"死循环等待"会冻结整个页面。非阻塞延时是 JavaScript 开发的基石。

回调形式:setTimeout

setTimeout 接受一个回调函数和延迟毫秒数,在指定时间后执行回调,期间不阻塞代码执行。

javascript console.log("现在执行");

setTimeout(() => { console.log("1秒后执行"); }, 1000);

console.log("这行立即执行,不会等待");

输出顺序:

现在执行 这行立即执行,不会等待 1秒后执行

async/await 形式:delay 辅助函数

如果 preference 异步链式写法,可用 async/await 配合 delay 函数(基于 Promise + setTimeout)。

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

async function example() { console.log("开始"); await delay(1000); console.log("1秒后继续"); }

example();

注意:JavaScript 没有内置 sleep 函数。所谓的"sleep 循环"(如 while (Date.now() - start < 1000) {})会完全阻塞主线程,导致页面无法响应,应严格避免。

来源:alvarotrigo.com

2.2 Java:CompletableFuture.delayedExecutor

Java 9 及以上版本提供 CompletableFuture.delayedExecutor,返回一个特殊的 Executor,提交的任务在指定时间后执行,而非立即执行。

基本用法

java import java.util.concurrent.*;

public class DelayedExecutorExample { public static void main(String[] args) { long start = System.currentTimeMillis(); System.out.println("[" + elapsed(start) + "ms] 准备提交延迟任务");

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

    CompletableFuture.runAsync(() -> {
        System.out.println("[" + elapsed(start) + "ms] 任务执行了!线程:"
            + Thread.currentThread().getName());
    }, delayed);

    System.out.println("[" + elapsed(start) + "ms] 主线程继续执行,没有被阻塞");

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

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

}

输出:

[0ms] 准备提交延迟任务 [0ms] 主线程继续执行,没有被阻塞 [2001ms] 任务执行了! 线程名:ForkJoinPool.commonPool-worker-1

指定自定义线程池

delayedExecutor 的三参数重载版本可指定延迟任务最终在哪个线程池执行:

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("任务执行!线程:" + Thread.currentThread().getName()); return "结果"; }, CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS, myPool));

实现场景:非阻塞重试

微服务调用失败后,通常需要等待一段时间再重试。使用 delayedExecutor 可避免在重试等待期间占用线程池:

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

注意:delayedExecutor 仅适合相对延迟的单次任务。需要每天固定时间执行或周期性任务时,请使用 ScheduledExecutorService。

来源:cnblogs.com

2.3 Kotlin 与 Android:协程的 delay() 与 LaunchedEffect

Kotlin 协程是 Android 开发中的轻量级并发方案。其 delay() 函数是非阻塞挂起函数——它挂起当前协程并释放底层线程,而非冻结线程。

在协程中使用 delay()

kotlin import kotlinx.coroutines.*

fun main() = runBlocking { launch { delay(1000L) println("Kotlin协程执行完毕!") } println("主协程继续执行...") }

Android Compose 中的 LaunchedEffect

在 Jetpack Compose 中,LaunchedEffect 用于在界面渲染完成后执行异步代码。其关键特性:不阻塞 UI 渲染。Compose 的界面绘制是同步完成的,LaunchedEffect 中的代码被推迟到主线程空闲后才执行。

kotlin @Composable fun MyScreen() { LaunchedEffect(Unit) { delay(500) println("延迟执行,UI 滑动完全流畅") } }

协程 vs 线程:每个协程内存占用约 2KB,传统线程至少占用 1MB 栈空间。协程可轻松创建成千上万个而不耗尽内存。

来源:jianshu.com、CSDN

2.4 嵌入式系统:从 Arduino 到 STM32 的非阻塞延时

在嵌入式开发中,阻塞式延时(如 delay())会导致 CPU 停转,无法同时处理多个任务。非阻塞延时是多任务编程的关键。

Arduino:使用 millis() 替代 delay()

millis() 返回从程序启动以来经过的毫秒数,利用它可在不阻塞的情况下实现定时操作。

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));
}

// 继续读取传感器、处理通信等
readSensors();    // illustrative — replace with actual code
updateDisplay();  // illustrative — replace with actual code

}

ESP32:FreeRTOS 的 vTaskDelay()

在 ESP32 的 FreeRTOS 环境中,vTaskDelay() 挂起当前任务并释放 CPU 给其他任务执行,从而实现非阻塞延时。

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:用定时器中断替代 HAL_Delay()

HAL_Delay() 基于 SysTick 定时器,通过空循环等待计数到指定值,会阻塞 CPU。更好的做法是使用硬件定时器中断:配置定时器在到达设定时间后触发中断,CPU 在等待期间继续执行主程序。

c // 简化概念示例:配置定时器,1秒后触发中断 // 在中断服务函数中设置标志位,主程序检测标志位执行相应操作 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { delay_complete = 1; // 设置非阻塞延时完成标志 TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }

来源:CSDN、wiki.lckfb.com、community.st.com

2.5 桌面与办公自动化:VBA 的 Application.OnTime

在 Excel 或 Office VBA 中,Application.Wait 会冻结整个 Office 应用程序。使用 Application.OnTime 可非阻塞地调度一个过程在指定时间执行。

vba ' 非阻塞式递增计数 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

与 JavaScript 的 setTimeout 非常相似。如需停止,调用 Application.OnTime 并将第四个参数设为 False。

来源:stegriff.co.uk

2.6 Linux 用户态:nanosleep 高精度延时

在 Linux 下,sleep() 和 usleep() 分别提供秒级和微秒级精度,但 usleep() 已被标记为过时。推荐使用 nanosleep(),提供纳秒级精度且不忙等待(不消耗 CPU 资源)。

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

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

若担心被信号中断,可检查剩余时间并重新调用:

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

来源:hzjcp.com

3. 如何选择适合你的非阻塞延时方案?

需求推荐方案
前端 / Node.js 定时setTimeout 或 async/await + delay()
Java 后台延迟任务CompletableFuture.delayedExecutor(适合单次相对延迟)
Android UI 延时LaunchedEffect + delay()(协程)
嵌入式多任务Arduino: millis();ESP32: vTaskDelay();STM32: 定时器中断
Office 自动化VBA Application.OnTime
Linux 高精度延时nanosleep()
周期性 / 绝对时间任务ScheduledExecutorService(Java)、setInterval(JS)等

注意事项:

  • 非阻塞延时通常无法做到绝对精确,实际执行时间可能因系统负载而有所偏差。
  • 某些环境(如 JavaScript setTimeout)在后台标签页中可能被节流。
  • 在嵌入式系统中,定时器中断的配置需结合硬件手册,避免与其他外设冲突。

4. 结语与下一步

非阻塞延时是现代编程中提升应用响应性和并发能力的重要手段。本文为你打开了这扇门,每个平台都有更深入的机制和最佳实践等待探索。建议立即选择一个与当前项目相关的代码片段进行尝试,观察引入非阻塞延时后程序的流畅度和资源利用率是否有所提升。

如需深入学习,可查阅各平台官方文档或相关社区讨论。