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

C#多线程同步游戏碰撞候选列表频繁为空问题求助

解决多线程碰撞候选列表同步问题

你的核心问题是:线程2在清空-填充共享列表的过程中,线程1高频率访问时会撞上列表为空的窗口,对象越多线程2计算耗时越长,这个空窗口出现的概率就越高。直接加锁无法解决,因为锁只能保证原子性,但无法掩盖「列表从空到填充完成」的时间差。

下面是具体的解决思路和实现方案:

1. 采用双缓冲(读写分离)架构

给每个对象维护两个候选列表:

  • currentCandidates:仅由线程1读取,用于当前帧的碰撞检测
  • nextCandidates:仅由线程2写入,用于存储下一轮的候选结果

线程2的工作流程调整为:

  • 清空自己负责的nextCandidates(无需锁,因为只有线程2操作这个列表)
  • 执行扫掠剪枝算法,将候选对象填充到nextCandidates
  • 填充完成后,加锁交换currentCandidates和nextCandidates的引用(这个操作是原子级的,耗时极短)

线程1的GetIntersections()只读取currentCandidates,全程不会遇到空列表——因为线程2填充的是备用列表,只有当新列表完全准备好后才会替换旧列表。

2. 优化锁的使用

  • 避免在整个填充过程中持有锁,只在交换列表引用的瞬间加锁,将锁的持有时间压缩到极致,减少线程1的等待时间。
  • 如果使用支持读写锁的语言(如C#的ReaderWriterLockSlim、C++的std::shared_mutex),可以进一步优化:线程1读取时用读锁(允许多个线程同时读),线程2交换时用写锁(互斥),最大化并发效率。

3. 伪代码示例(C#风格)

public class CollisionObject
{
    private List<CollisionObject> _currentCandidates = new();
    private List<CollisionObject> _nextCandidates = new();
    private readonly ReaderWriterLockSlim _rwLock = new();

    // 线程1调用:获取当前可用的候选列表
    public List<CollisionObject> GetCollisionCandidates()
    {
        _rwLock.EnterReadLock();
        try
        {
            return _currentCandidates;
        }
        finally
        {
            _rwLock.ExitReadLock();
        }
    }

    // 线程2调用:添加候选到备用列表
    public void AddNextCandidate(CollisionObject candidate)
    {
        // 仅线程2操作,无需锁
        _nextCandidates.Add(candidate);
    }

    // 线程2调用:完成填充后交换列表
    public void SwapCandidateLists()
    {
        _rwLock.EnterWriteLock();
        try
        {
            // 交换引用
            (_currentCandidates, _nextCandidates) = (_nextCandidates, _currentCandidates);
            // 清空备用列表,准备下一轮填充
            _nextCandidates.Clear();
        }
        finally
        {
            _rwLock.ExitWriteLock();
        }
    }
}

// 线程2的主循环
void CollisionCandidateThread()
{
    while (GameRunning)
    {
        // 1. 清空所有对象的备用列表
        foreach (var obj in AllCollisionObjects)
        {
            obj._nextCandidates.Clear();
        }

        // 2. 执行扫掠剪枝,填充备用列表
        SweepAndPrune(AllCollisionObjects);

        // 3. 批量交换所有对象的候选列表
        foreach (var obj in AllCollisionObjects)
        {
            obj.SwapCandidateLists();
        }

        // 等待下一轮计算周期(根据需求调整)
        Thread.Sleep(CalculateIntervalMs);
    }
}

// 线程1的Update流程
void MainThreadUpdate()
{
    foreach (var obj in AllCollisionObjects)
    {
        obj.Update();
    }
}

// 碰撞对象的Update方法
void Update()
{
    var candidates = GetCollisionCandidates();
    foreach (var candidate in candidates)
    {
        // 执行实际碰撞检测逻辑
        PerformPreciseCollisionCheck(candidate);
    }
}

4. 额外优化点

  • 如果线程2的计算耗时过长(比如对象数量极多),可以考虑将扫掠剪枝算法拆分为多个子任务并行处理,进一步缩短填充时间。
  • 初始化阶段,currentCandidates可以设置为默认空列表或初始候选,避免游戏启动时线程1访问空列表。

内容的提问来源于stack exchange,提问作者AudioGuy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 10:12:09