Unity中C# Lock在协程中失效的Texture缓存问题求解
为啥会出这问题?
Unity协程里的lock遇到yield return就会自动释放——因为协程暂停的时候会退出lock块。第一个协程在等下载的那段时间,锁早就松了,其他协程直接钻进lock里,发现缓存还是空的,就跟着一起下载,最后俩都往缓存里加同一个key,自然就炸了。
怎么改?
得加个「待处理请求队列」,记着哪些URL正在下载,同一URL的后续请求别瞎下载,等着就行,等第一个下载完了,直接拿缓存的结果给所有等的人。
修改后的完整代码
using System; using System.Collections; using System.Collections.Generic; using UnityEngine.Networking; using UnityEngine; public class TextureCache : MonoBehaviour { struct CacheData { public Texture2D texture; public long expiry; }; private static Dictionary<string, CacheData> cache = new Dictionary<string, CacheData>(); // 存正在下载的URL和对应的等待回调列表 private static Dictionary<string, List<Action<Texture2D>>> pendingRequests = new Dictionary<string, List<Action<Texture2D>>>(); private static readonly object cacheLock = new object(); private static void Add(string key, Texture2D texture, long ttl) { CacheData data = new CacheData(); data.texture = texture; data.expiry = DateTimeOffset.Now.ToUnixTimeSeconds() + ttl; Debug.Log("cache adding -- " + key); // 用TryAdd防重复,双重保险 if (!cache.TryAdd(key, data)) { Debug.LogWarning("Cache key already exists: " + key); } } private static Texture2D GetFromCache(string key) { lock (cacheLock) { if (!cache.TryGetValue(key, out var data)) { Debug.Log("cache not found " + key); return null; } // 检查过期 if (DateTimeOffset.Now.ToUnixTimeSeconds() > data.expiry) { Debug.Log("cache expired " + key); cache.Remove(key); return null; } Debug.Log("cache got " + key); return data.texture; } } public static IEnumerator Get(string key, Action<Texture2D> callback) { // 先查缓存 Texture2D texture = GetFromCache(key); if (texture != null) { callback(texture); yield break; } lock (cacheLock) { // 进锁再查一遍,防止中间有人已经把缓存加上了 texture = GetFromCache(key); if (texture != null) { callback(texture); yield break; } // 看看是不是已经有人在下载这个URL了 if (pendingRequests.TryGetValue(key, out var callbacks)) { // 把当前回调加进等待队列就行 callbacks.Add(callback); Debug.Log("Added to pending requests for: " + key); } else { // 没人下载,建个新队列,开始下载 pendingRequests.Add(key, new List<Action<Texture2D>> { callback }); Debug.Log("cache downloading " + key); // 先解锁再下载,别拿着锁卡主线程 yield return DownloadTexture(key); } } // 没抢到下载权的,就在这等,直到缓存有了或者下载失败 while (true) { texture = GetFromCache(key); if (texture != null || !pendingRequests.ContainsKey(key)) { callback(texture); break; } // 每帧查一次,别卡死 yield return null; } } private static IEnumerator DownloadTexture(string key) { UnityWebRequest www = UnityWebRequestTexture.GetTexture(key); yield return www.SendWebRequest(); Texture2D texture = null; if (www.result == UnityWebRequest.Result.Success) { Debug.Log("cache downloaded " + key); texture = ((DownloadHandlerTexture)www.downloadHandler).texture as Texture2D; if (texture != null) { Debug.Log("cache adding " + key); Add(key, texture, 300); } } else { Debug.Log("Download failed: " + www.error); } // 下载完了,通知所有等着的回调 lock (cacheLock) { if (pendingRequests.TryGetValue(key, out var callbacks)) { foreach (var cb in callbacks) { cb(texture); } pendingRequests.Remove(key); } } www.Dispose(); } }
改了啥重点?
- 加了
pendingRequests字典:专门记正在下载的URL,后面来的请求直接排队,不重复下载。 - 拆分下载逻辑:把下载的代码单独拎到
DownloadTexture里,确保锁只在检查状态的时候用,不会在等下载的时候占着锁不放。 - 双重缓存检查:进锁前后都查一遍缓存,防止漏了别人刚加的缓存。
- 等待队列处理:后续请求不用自己下载,等着第一个下载完了拿结果就行。
- 去掉多余的
callback(null):原代码末尾的这个会让回调被调用两次,直接删了。 - 用
TryAdd/TryGetValue替代Add/ContainsKey:减少字典操作的异常,代码更稳。
调用代码不用改
原来的调用代码直接用就行,不用动。
内容的提问来源于stack exchange,提问作者aquaman
相关产品推荐
相关产品推荐

