在软件开发中,我们经常需要让程序"等一会儿"再执行某个操作:重试失败的网络请求、定时刷新数据、控制硬件闪烁。许多初学者使用的延时函数(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. 结语与下一步
非阻塞延时是现代编程中提升应用响应性和并发能力的重要手段。本文为你打开了这扇门,每个平台都有更深入的机制和最佳实践等待探索。建议立即选择一个与当前项目相关的代码片段进行尝试,观察引入非阻塞延时后程序的流畅度和资源利用率是否有所提升。
如需深入学习,可查阅各平台官方文档或相关社区讨论。