如何在C#中实现不阻塞主线程的类Unity Update定时循环
实现非阻塞的Unity-style Update循环(指定帧率)
你的现有代码有两个关键问题:一是Task.Delay(tickrate / 60)没有用await,导致延迟完全不生效,循环会全速执行;二是整个方法是同步的,会一直占用调用线程,无法做到非阻塞。
下面是几种可行的解决方案:
基础非阻塞实现
把方法改成异步模式,通过async/await让线程在延迟期间被释放,去处理其他任务:
// 注意方法签名改为async Task,表明这是异步方法 public async Task UpdateAsync(int tickrate) { // 计算每帧的间隔毫秒数:tickrate为每秒需要执行的次数 int frameIntervalMs = 1000 / tickrate; while (true) { // 执行你的更新逻辑 foreach (IAgent agent in Agents) { agent.Move(); } // await会释放当前线程,到时间后再回到循环 await Task.Delay(frameIntervalMs); } }
调用时直接启动即可,不会阻塞调用线程:
// 在异步上下文外可以用下划线忽略返回的Task,或者用await(如果在异步方法内) _ = UpdateAsync(60);
高精度帧率控制方案
Task.Delay的精度有限(通常在10-15ms左右),如果需要更精准的帧率,可以用Stopwatch计算每帧的实际耗时,动态调整延迟时间:
public async Task UpdateAsync(int tickrate) { int targetFrameIntervalMs = 1000 / tickrate; var stopwatch = Stopwatch.StartNew(); long lastFrameEndTime = 0; while (true) { // 执行更新逻辑 foreach (IAgent agent in Agents) { agent.Move(); } // 计算当前帧耗时,调整延迟 long currentTime = stopwatch.ElapsedMilliseconds; long elapsedSinceLastFrame = currentTime - lastFrameEndTime; long delayMs = targetFrameIntervalMs - elapsedSinceLastFrame; // 避免延迟为负数,确保线程能让出 if (delayMs > 0) { await Task.Delay((int)delayMs); } else { await Task.Yield(); // 强制让出线程,避免完全占用CPU } lastFrameEndTime = stopwatch.ElapsedMilliseconds; } }
支持中途停止的版本
如果需要在运行时终止循环,不要用while(true),改用CancellationToken实现可取消的循环:
public async Task UpdateAsync(int tickrate, CancellationToken cancellationToken) { int targetFrameIntervalMs = 1000 / tickrate; var stopwatch = Stopwatch.StartNew(); long lastFrameEndTime = 0; // 检查取消令牌,判断是否继续循环 while (!cancellationToken.IsCancellationRequested) { foreach (IAgent agent in Agents) { agent.Move(); } long currentTime = stopwatch.ElapsedMilliseconds; long elapsedSinceLastFrame = currentTime - lastFrameEndTime; long delayMs = targetFrameIntervalMs - elapsedSinceLastFrame; if (delayMs > 0) { // 传递取消令牌,支持中途终止延迟 await Task.Delay((int)delayMs, cancellationToken); } else { await Task.Yield(); } lastFrameEndTime = stopwatch.ElapsedMilliseconds; } }
调用示例:
var cts = new CancellationTokenSource(); // 启动循环 _ = UpdateAsync(60, cts.Token); // 需要停止时调用 cts.Cancel();
额外注意点
- 你原来代码里的
tickrate / 60逻辑有问题:如果tickrate是每秒执行的次数,正确的间隔计算应该是1000 / tickrate(毫秒)。 - 异步方法不要用
async void,除非是事件处理程序,否则用async Task方便后续处理。
内容的提问来源于stack exchange,提问作者Hale Jan
相关产品推荐
相关产品推荐

