前言
在Unity中设计线程安全的Lookup Table(查找表)时,需要兼顾线程安全和Unity引擎的单线程特性。以下是一个完整的设计方案,包含实现代码、注意事项和最佳实践:
对惹,这里有一个游戏开发交流小组 ,希望大家可以点击进来一起交流一下开发经验呀!
设计思路
- 使用
ConcurrentDictionary
作为基础容器(.NET 4.x及以上) - 主线程与工作线程分离:工作线程填充数据,主线程消费数据
- 双缓冲机制:避免读写冲突
- 支持异步初始化(如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;
}
}
}
关键设计要点
- 双缓冲机制
-
工作字典:后台线程写入(资源加载/网络请求)
-
主字典:主线程只读访问
-
通过
SwapBuffers()
原子性交换引用 -
线程安全保证
-
ConcurrentDictionary
:保证单字典操作的线程安全 -
lock(_swapLock)
:确保缓冲区交换的原子性 -
值类型/不可变对象优先(避免引用共享问题)
-
Unity资源特殊处理
-
禁止跨线程操作Unity API:资源加载结果通过
AddDataSafe
注入 -
异步加载:使用
Addressables
或Task.Run
封装 -
主线程消费:渲染、实例化等操作在
TryGetData
后执行 -
缓冲区交换策略
-
在
Update()
中定期交换(简单场景) -
基于事件驱动交换(如加载完成事件)
-
手动控制交换时机(如场景切换时)
注意事项
- 内存开销
-
双缓冲导致2倍内存占用(临时)
-
大容量表需评估内存影响
-
资源生命周期
-
卸载资源时使用
Addressables.Release
-
实现
IDisposable
接口管理非托管资源 -
值类型优化
// 结构体存储优化示例
public struct TextureData {
public int Width;
public int Height;
public byte[] RawData;
}
private ThreadSafeLookupTable<string, TextureData> _dataTable;
- 异常处理
- 异步操作添加
try-catch
- 关键操作返回状态码
替代方案对比
方法 | 优点 | 缺点 |
---|---|---|
双缓冲+ConcurrentDictionary | 读写分离,高并发安全 | 双倍内存开销 |
ReaderWriterLockSlim | 细粒度锁控制 | 死锁风险,代码复杂 |
ImmutableDictionary | 无锁读取 | 写操作性能低 |
最佳实践
- 避免频繁交换:每帧交换改为按需交换
- 结合对象池:复用已加载资源
- 版本控制:添加数据版本号检测更新
- 性能分析:Profiler监控
SwapBuffers()
耗时
此设计在Unity 2020 LTS+ .NET 4.x环境下验证通过,可扩展为通用资源管理系统核心组件。
更多教学视频