c# - 每个方法的线程同步锁定

标签 c# caching thread-safety

我想为 ASP.NET MVC 站点创建一个静态 Cached 类,以便快速访问下拉列表等缓存项目。它需要实现锁定,以便当 key 返回为空时,可以从存储库中提取它,同时任何其他请求线程等待它返回。因此,它需要每个方法的线程锁定(而不是共享锁)。我的第一个想法是使用 nameof 作为每个方法的锁,而不是创建一个单独的对象来为每个方法加锁。简化版本看起来像......

public static class Cached
{
    public static List<Country> GetCountriesList()
    {
        List<Country> cacheItem = null;

        if (HttpContext.Current.Cache["CountriesList"] != null)
            cacheItem = (List<Country>)HttpContext.Current.Cache["CountriesList"];
        else
        {
            lock (nameof(GetCountriesList))            
            {
                // Check once more in case it got stored while waiting on the lock
                if (HttpContext.Current.Cache["CountriesList"] == null)    
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = repo.SelectCountries();
                        HttpContext.Current.Cache.Insert("CountriesList", cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
                else
                    cacheItem = (List<Country>)HttpContext.Current.Cache["CountriesList"];
            }
        }

        return cacheItem;
    }

    public static List<State> GetStatesList()
    {
        List<State> cacheItem = null;

        if (HttpContext.Current.Cache["StatesList"] != null)
            cacheItem = (List<State>)HttpContext.Current.Cache["StatesList"];
        else
        {
            lock (nameof(GetStatesList))            
            {
                // Check once more in case it got stored while waiting on the lock
                if (HttpContext.Current.Cache["StatesList"] == null)    
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = repo.SelectStates();
                        HttpContext.Current.Cache.Insert("StatesList", cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
                else
                    cacheItem = (List<State>)HttpContext.Current.Cache["StatesList"];
            }
        }

        return cacheItem;
    }
}

这样的方法有什么明显的错误吗?

更新:

根据建议,锁定字符串是一个坏主意,我已将其更改为我在 SO 的 Opserver 代码中找到的模式,该代码使用 ConcurrentDictionary 来为每个缓存存储一个锁定对象 key 。以下内容是否有问题:

public static class Cached
{
    private static readonly ConcurrentDictionary<string, object> _cacheLocks = new ConcurrentDictionary<string, object>();

    private const string KEY_COUNTRIES_LIST = "CountriesList";
    public static List<Country> GetCountriesList()
    {
        List<Country> cacheItem = null;
        var nullLoadLock = _cacheLocks.AddOrUpdate(KEY_COUNTRIES_LIST, k => new object(), (k, old) => old);

        if (HttpContext.Current.Cache[KEY_COUNTRIES_LIST] != null)
            cacheItem = (List<Country>)HttpContext.Current.Cache[KEY_COUNTRIES_LIST];
        else
        {
            lock (nullLoadLock)
            {
                // Check once more in case it got stored while waiting on the lock
                if (HttpContext.Current.Cache[KEY_COUNTRIES_LIST] == null)
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = repo.SelectCountries();
                        HttpContext.Current.Cache.Insert(KEY_COUNTRIES_LIST, cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
                else
                    cacheItem = (List<Country>)HttpContext.Current.Cache[KEY_COUNTRIES_LIST];
            }
        }

        return cacheItem;
    }

    private const string KEY_STATES_LIST = "StatesList";
    public static List<State> GetStatesList()
    {
        List<State> cacheItem = null;
        var nullLoadLock = _cacheLocks.AddOrUpdate(KEY_COUNTRIES_LIST, k => new object(), (k, old) => old);

        if (HttpContext.Current.Cache[KEY_STATES_LIST] != null)
            cacheItem = (List<State>)HttpContext.Current.Cache[KEY_STATES_LIST];
        else
        {
            lock (nullLoadLock)
            {
                // Check once more in case it got stored while waiting on the lock
                if (HttpContext.Current.Cache[KEY_STATES_LIST] == null)
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = repo.SelectStates();
                        HttpContext.Current.Cache.Insert(KEY_STATES_LIST, cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
                else
                    cacheItem = (List<State>)HttpContext.Current.Cache[KEY_STATES_LIST];
            }
        }

        return cacheItem;
    }
}

最佳答案

根据您迄今为止发布的内容,我认为您想得太多了。 :) 我认为没有必要用锁定对象填充另一个字典。由于您在显式命名的方法中使用它们,因此只需根据需要将它们声明为字段即可。

首先,建议不要锁定 string值是合理的,但基于两个 string 的问题值可以看起来相同,但仍然是不同的对象。您可以通过存储适当的 string 在您的场景中避免这种情况。 const 中的值字段:

public static class Cached
{
    private const string _kcountries = "CountriesList";
    private const string _kstates = "StatesList";

    public static List<Country> GetCountriesList()
    {
        List<Country> cacheItem = (List<Country>)HttpContext.Current.Cache[_kcountries];

        if (cacheItem == null)
        {
            lock (_kcountries)
            {
                // Check once more in case it got stored while waiting on the lock
                cacheItem = (List<Country>)HttpContext.Current.Cache[_kcountries];

                if (cacheItem == null)    
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = repo.SelectCountries();
                        HttpContext.Current.Cache.Insert(_kcountries, cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
            }
        }

        return cacheItem;
    }

    public static List<State> GetStatesList()
    {
        // Same as above, except using _kstates instead of _kcountries
    }
}

请注意,无论如何,您都不应该在整个代码中使用字符串文字。定义 const 是更好的做法。字段来表示这些值。所以你用一 block 石头杀死两只鸟,执行上述操作。 :)

剩下的唯一问题是,您仍然使用可能是公共(public)的值来锁定,因为字符串文字是被保留的,并且如果在其他地方使用完全相同的字符串,它也可能是相同的保留值。这是一个值得商榷的问题;我倾向于避免这样做,以确保我控制之外的其他代码不会获取我的代码尝试使用的相同锁,但有些人认为这种担忧被夸大了。 YMMV。 :)

如果您确实(像我一样)关心使用可能公开的值,那么您可以关联一个唯一的 object值而不是使用字符串引用:

public static class Cached
{
    private const string _kcountriesKey = "CountriesList";
    private const string _kstatesKey = "StatesList";

    private static readonly object _kcountriesLock = new object();
    private static readonly object _kstatesLock = new object();

    public static List<Country> GetCountriesList()
    {
        List<Country> cacheItem = (List<Country>)HttpContext.Current.Cache[_kcountriesKey];

        if (cacheItem == null)
        {
            lock (_kcountriesLock)
            {
                // Check once more in case it got stored while waiting on the lock
                cacheItem = (List<Country>)HttpContext.Current.Cache[_kcountriesKey];

                if (cacheItem == null)    
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = repo.SelectCountries();
                        HttpContext.Current.Cache.Insert(_kcountriesKey, cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
            }
        }

        return cacheItem;
    }

    // etc.
}

即使用...Key缓存字段(因为它确实需要 string 键值),但 ...Lock用于锁定的字段(这样您就可以确保您控制之外的任何代码都无法访问用于锁定的对象值)。

我会注意到,您确实有机会通过编写一个 Get...() 来减少代码中的重复。可以由您的各种类型的数据共享的实现:

public static class Cached
{
    private const string _kcountriesKey = "CountriesList";
    private const string _kstatesKey = "StatesList";

    private static readonly object _kcountriesLock = new object();
    private static readonly object _kstatesLock = new object();

    public static List<Country> GetCountriesList()
    {
        // Assuming SelectCountries() is in fact declared to return List<Country>
        // then you should actually be able to omit the type parameter in the method
        // call and let type inference figure it out. Same thing for the call to
        // _GetCachedData<State>() in the GetStatesList() method.
        return _GetCachedData<Country>(_kcountriesKey, _kcountriesLock, repo => repo.SelectCountries());
    }

    public static List<State> GetStatesList()
    {
        return _GetCachedData<State>(_kstatesKey, _kstatesLock, repo => repo.SelectStates());
    }

    private static List<T> _GetCachedData<T>(string key, object lockObject, Func<Repository, List<T>> selector)
    {
        List<T> cacheItem = (List<T>)HttpContext.Current.Cache[key];

        if (cacheItem == null)
        {
            lock (lockObject)
            {
                // Check once more in case it got stored while waiting on the lock
                cacheItem = (List<T>)HttpContext.Current.Cache[key];

                if (cacheItem == null)    
                {
                    using (var repo = new Repository())
                    {
                        cacheItem = selector(repo);
                        HttpContext.Current.Cache.Insert(key, cacheItem, null, DateTime.Now.AddHours(2), TimeSpan.Zero);
                    }
                }
            }
        }

        return cacheItem;
    }

    // etc.
}

最后,我会注意到,由于底层缓存(即 System.Web.Caching.Cache )是线程安全的,您可以完全跳过所有这些,而是​​选择盲目填充缓存,如果您的项目( List<T> )有问题)未找到。唯一的缺点是,在某些情况下,您可能会多次检索同一列表。好处是代码要简单得多。

关于c# - 每个方法的线程同步锁定,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35557698/

相关文章:

c# - 将 TextWriter 与 StreamWriter 一起使用并同时读取/写入

c# - 如何从 Linq-To-Sql 映射中排除成员?

ruby-on-rails - Rails 3.1 Sweeper 用于不同命名空间中的 Controller 方法

asp.net - 在服务器上缓存完整响应

php - 如何禁用缓存 100%

android - 检查服务器上是否存在 URL

java - 同步链表 - peek

c# - Azure 存储和条件替换/合并

c# - Visual Studio 2010 安装项目中的 "Runtime Implementation from ASSEMBLE NAME"是什么? (vs 2015 中的安装程序扩展设置项目)

c# - 如何使公共(public)属性线程安全?