.NET自定义游戏循环下如何将跨线程操作切换至主线程执行
跨线程游戏操作转发到主线程实现方案
已知前提
- 游戏循环运行在控制台应用的主线程上
- 游戏操作/事件由其他线程触发
待解决问题
- 如何将这些跨线程触发的操作/事件切换、转发到主线程执行?
现有实现的不足
当前基于自定义TaskScheduler的实现存在三个明显问题:
- 自定义
TaskScheduler实现逻辑复杂,容易隐藏边界bug - 未配置
SynchronizationContext,异步代码的线程切换需要手动处理 - 游戏循环采用固定N毫秒间隔的Tick模式,新到的操作/事件必须等下一个Tick才能处理,响应实时性差
原有实现代码
程序入口
class Program { private static async Task Main(string[] args) { var game = new Game(); // 模拟其他线程定时触发操作、事件 new Timer( i => game.ActionAsync().Wait(), null, 0, 3000 ); new Timer( i => game.EventAsync().Wait(), null, 0, 3000 ); game.Run(); } }
游戏核心类
public class Game { private GameTaskScheduler TaskScheduler { get; } = new GameTaskScheduler(); public Game() {} // 游戏主循环 public void Run() { while (true) { TaskScheduler.Execute(); // 游戏逻辑执行 Console.WriteLine( "Update: {0:00}", Environment.CurrentManagedThreadId ); Thread.Sleep( 1000 ); } } // 用户操作入口 public async Task ActionAsync() { await SwitchToMainThread(); Console.WriteLine( "Action: {0:00}", Environment.CurrentManagedThreadId ); } // 系统事件入口 public async Task EventAsync() { await SwitchToMainThread(); Console.WriteLine( "Event: {0:00}", Environment.CurrentManagedThreadId ); } // 线程切换工具方法 public AwaitExtensions.TaskSchedulerAwaitable SwitchToMainThread() { return TaskScheduler.SwitchTo(); } }
自定义任务调度器
// 待优化点:能否不自定义TaskScheduler实现需求? internal class GameTaskScheduler : TaskScheduler { private ConcurrentQueue<Task> Tasks { get; } = new ConcurrentQueue<Task>(); public override int MaximumConcurrencyLevel => 1; public void Execute() { while (Tasks.TryDequeue( out var task )) { TryExecuteTask( task ); } } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; } protected override void QueueTask(Task task) { Tasks.Enqueue( task ); } protected sealed override bool TryDequeue(Task task) { return false; } protected override IEnumerable<Task>? GetScheduledTasks() { return Tasks.ToArray(); } }
优化实现方案
不需要自定义容易出问题的TaskScheduler,基于官方原生的SynchronizationContext扩展点,搭配线程安全队列和等待信号量即可实现,同时解决实时响应问题:
- 用
ConcurrentQueue存储待主线程执行的回调,搭配AutoResetEvent做信号通知,新任务入队时立刻发送信号唤醒主线程 - 实现单线程专属的
SynchronizationContext,重写Post方法完成回调入队、信号触发逻辑;主线程启动时将该上下文设为当前线程的默认同步上下文,后续所有await默认会自动切回主线程执行,无需手动编写线程切换代码 - 替换原固定
Thread.Sleep的等待逻辑,改为带超时的信号等待:超时时间设为Tick间隔,两种场景都会唤醒主线程:一是到达固定Tick时间需要执行游戏逻辑,二是有新的跨线程任务到达需要即时处理
核心实现代码
首先是自定义单线程同步上下文:
public class GameSyncContext : SynchronizationContext { private readonly ConcurrentQueue<(SendOrPostCallback callback, object? state)> _workQueue = new(); private readonly AutoResetEvent _workSignal = new(false); /// <summary> /// 等待新任务到达,超时则自动唤醒执行Tick逻辑 /// </summary> public void Wait(int timeoutMs) => _workSignal.WaitOne(timeoutMs); /// <summary> /// 执行队列中所有待处理任务 /// </summary> public void RunPendingJobs() { while (_workQueue.TryDequeue(out var job)) { job.callback(job.state); } } public override void Post(SendOrPostCallback d, object? state) { _workQueue.Enqueue((d, state)); _workSignal.Set(); } public override void Send(SendOrPostCallback d, object? state) { throw new NotSupportedException("单线程游戏上下文不支持同步Send调用,请使用Post"); } }
修改后的游戏主循环:
public class Game { private readonly GameSyncContext _mainThreadSyncContext = new(); private const int TickInterval = 1000; public void Run() { // 绑定主线程同步上下文 SynchronizationContext.SetSynchronizationContext(_mainThreadSyncContext); long lastTickTimestamp = Environment.TickCount64; while (true) { // 先执行所有待处理的跨线程任务 _mainThreadSyncContext.RunPendingJobs(); long now = Environment.TickCount64; long timeSinceLastTick = now - lastTickTimestamp; if (timeSinceLastTick >= TickInterval) { // 到达Tick间隔,执行游戏帧逻辑 Console.WriteLine($"Update: {Environment.CurrentManagedThreadId}"); lastTickTimestamp = now; timeSinceLastTick = 0; } // 计算等待时长:没有新任务就等到下一个Tick,有新任务立刻唤醒 int waitTime = (int)Math.Max(0, TickInterval - timeSinceLastTick); _mainThreadSyncContext.Wait(waitTime); } } // 跨线程触发的操作/事件无需手动切线程,await后自动回到主线程 public async Task ActionAsync() { await Task.Yield(); Console.WriteLine($"Action: {Environment.CurrentManagedThreadId}"); } public async Task EventAsync() { await Task.Yield(); Console.WriteLine($"Event: {Environment.CurrentManagedThreadId}"); } }
方案优势
- 基于官方提供的
SynchronizationContext扩展点实现,逻辑简单清晰,不需要处理TaskScheduler复杂的内部契约,不容易出边界bug - 异步代码自动捕获同步上下文,无需手动编写线程切换的辅助方法
- 无新任务时主线程阻塞等待不占用CPU资源,新任务到达时立刻唤醒执行,不需要等到下一个固定Tick,响应延迟更低
内容的提问来源于stack exchange,提问作者Denis535
相关产品推荐
相关产品推荐

