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

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();
    }
}
改了啥重点?
  1. 加了pendingRequests字典:专门记正在下载的URL,后面来的请求直接排队,不重复下载。
  2. 拆分下载逻辑:把下载的代码单独拎到DownloadTexture里,确保锁只在检查状态的时候用,不会在等下载的时候占着锁不放。
  3. 双重缓存检查:进锁前后都查一遍缓存,防止漏了别人刚加的缓存。
  4. 等待队列处理:后续请求不用自己下载,等着第一个下载完了拿结果就行。
  5. 去掉多余的callback(null):原代码末尾的这个会让回调被调用两次,直接删了。
  6. 用TryAdd/TryGetValue替代Add/ContainsKey:减少字典操作的异常,代码更稳。
调用代码不用改

原来的调用代码直接用就行,不用动。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 03:25:22