You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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扩展点,搭配线程安全队列和等待信号量即可实现,同时解决实时响应问题:

  1. 用ConcurrentQueue存储待主线程执行的回调,搭配AutoResetEvent做信号通知,新任务入队时立刻发送信号唤醒主线程
  2. 实现单线程专属的SynchronizationContext,重写Post方法完成回调入队、信号触发逻辑;主线程启动时将该上下文设为当前线程的默认同步上下文,后续所有await默认会自动切回主线程执行,无需手动编写线程切换代码
  3. 替换原固定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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:22:17