Unity线程安全LookupTable设计方案

2 阅读3分钟

前言

在Unity中设计线程安全的Lookup Table(查找表)时,需要兼顾线程安全和Unity引擎的单线程特性。以下是一个完整的设计方案,包含实现代码、注意事项和最佳实践:

对惹,这里有一个游戏开发交流小组 ,希望大家可以点击进来一起交流一下开发经验呀!

设计思路

  1. 使用ConcurrentDictionary作为基础容器(.NET 4.x及以上)
  2. 主线程与工作线程分离:工作线程填充数据,主线程消费数据
  3. 双缓冲机制:避免读写冲突
  4. 支持异步初始化(如Addressables资源加载)

实现代码

using System.Collections.Concurrent;
using UnityEngine;
using System.Threading.Tasks;

public class ThreadSafeLookupTable<TKey, TValue>
{
    // 双缓冲:工作字典(后台线程写入) + 主字典(主线程读取)
    private ConcurrentDictionary<TKey, TValue> _workDictionary = new();
    private ConcurrentDictionary<TKey, TValue> _mainDictionary = new();

    // 线程同步锁(用于交换缓冲区)
    private readonly object _swapLock = new();

    /// <summary> 工作线程安全添加数据 </summary>
    public void AddDataSafe(TKey key, TValue value)
    {
        _workDictionary[key] = value;
    }

    /// <summary> 交换缓冲区(主线程调用) </summary>
    public void SwapBuffers()
    {
        lock (_swapLock)
        {
            (_mainDictionary, _workDictionary) = (_workDictionary, _mainDictionary);
            _workDictionary.Clear(); // 清空工作缓冲区
        }
    }

    /// <summary> 主线程安全读取数据 </summary>
    public bool TryGetData(TKey key, out TValue value)
    {
        return _mainDictionary.TryGetValue(key, out value);
    }

    // 异步加载示例(如Addressables)
    public async Task LoadAssetAsync<TAsset>(TKey key, string assetPath) where TAsset : UnityEngine.Object
    {
        var loadOp = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<TAsset>(assetPath);
        await loadOp.Task;
        
        if (loadOp.Status == UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationStatus.Succeeded)
        {
            AddDataSafe(key, (TValue)(object)loadOp.Result);
        }
    }
}

使用示例

public class ExampleUsage : MonoBehaviour
{
    private ThreadSafeLookupTable<string, Texture> _textureTable = new();

    void Start()
    {
        // 后台线程加载资源
        Task.Run(async () => {
            await _textureTable.LoadAssetAsync<Texture>("bg", "Assets/Textures/bg.png");
        });
    }

    void Update()
    {
        // 每帧交换缓冲区(或按需调用)
        _textureTable.SwapBuffers();

        // 主线程安全使用数据
        if (_textureTable.TryGetData("bg", out Texture tex))
        {
            GetComponent<Renderer>().material.mainTexture = tex;
        }
    }
}

关键设计要点

  1. 双缓冲机制
  • 工作字典:后台线程写入(资源加载/网络请求)

  • 主字典:主线程只读访问

  • 通过SwapBuffers()原子性交换引用

  • 线程安全保证

  • ConcurrentDictionary:保证单字典操作的线程安全

  • lock(_swapLock):确保缓冲区交换的原子性

  • 值类型/不可变对象优先(避免引用共享问题)

  • Unity资源特殊处理

  • 禁止跨线程操作Unity API:资源加载结果通过AddDataSafe注入

  • 异步加载:使用AddressablesTask.Run封装

  • 主线程消费:渲染、实例化等操作在TryGetData后执行

  • 缓冲区交换策略

  • Update()中定期交换(简单场景)

  • 基于事件驱动交换(如加载完成事件)

  • 手动控制交换时机(如场景切换时)

注意事项

  1. 内存开销
  • 双缓冲导致2倍内存占用(临时)

  • 大容量表需评估内存影响

  • 资源生命周期

  • 卸载资源时使用Addressables.Release

  • 实现IDisposable接口管理非托管资源

  • 值类型优化

// 结构体存储优化示例
public struct TextureData {
    public int Width;
    public int Height;
    public byte[] RawData;
}
private ThreadSafeLookupTable<string, TextureData> _dataTable;
  1. 异常处理
  • 异步操作添加try-catch
  • 关键操作返回状态码

替代方案对比

方法优点缺点
双缓冲+ConcurrentDictionary读写分离,高并发安全双倍内存开销
ReaderWriterLockSlim细粒度锁控制死锁风险,代码复杂
ImmutableDictionary无锁读取写操作性能低

最佳实践

  1. 避免频繁交换:每帧交换改为按需交换
  2. 结合对象池:复用已加载资源
  3. 版本控制:添加数据版本号检测更新
  4. 性能分析:Profiler监控SwapBuffers()耗时

此设计在Unity 2020 LTS+ .NET 4.x环境下验证通过,可扩展为通用资源管理系统核心组件。

更多教学视频

Unity3D​www.bycwedu.com/promotion_channels/2146264125