【问题标题】:Why is the value returned from a ConcurrentDictionary always null when multiple concurrent threads are used?为什么在使用多个并发线程时,从 ConcurrentDictionary 返回的值始终为 null?
【发布时间】:2016-06-02 23:20:50
【问题描述】:

我已经实现了一个由 ConcurrentDictionary 支持的简单内存缓存

public class MemoryCache 
{
    private ConcurrentDictionary<string, CacheObject> _memory;

    public MemoryCache()
    {
        this._memory = new ConcurrentDictionary<string, CacheObject>();
    }


    private bool TryGetValue(string key, out CacheObject entry)
    {
        return this._memory.TryGetValue(key, out entry);
    }

    private bool CacheAdd(string key, object value, DateTime? expiresAt = null)
    {
        CacheObject entry;
        if (this.TryGetValue(key, out entry)) return false;

        entry = new CacheObject(value, expiresAt);
        this.Set(key, entry);

        return true;

    }

    public object Get(string key)
    {
        long lastModifiedTicks;
        return Get(key, out lastModifiedTicks);
    }

    public object Get(string key, out long lastModifiedTicks)
    {
        lastModifiedTicks = 0;
        CacheObject CacheObject;
        if (this._memory.TryGetValue(key, out CacheObject))
        {
            if (CacheObject.HasExpired)
            {
                this._memory.TryRemove(key, out CacheObject);
                return null;
            }
            lastModifiedTicks = CacheObject.LastModifiedTicks;
            return CacheObject.Value;
        }
        return null;
    }

    public T Get<T>(string key)
    {
        var value = Get(key);
        if (value != null) return (T)value;
        return default(T);
    }


    public bool Add<T>(string key, T value)
    {
        return CacheAdd(key, value);
    }
}

现在我正在尝试使用基于@ayende 的blog post 的代码对其进行测试。

var w = new ManualResetEvent(false);
var threads = new List<Thread>();
for (int i = 0; i < Environment.ProcessorCount; i++)
{
    threads.Add(new Thread(() =>
    {
        w.WaitOne();
        DoWork(i);
    }));
    threads.Last().Start();
}

w.Set();//release all threads to start at the same time
foreach (var thread in threads)
{
    thread.Join();
} 

所以 DoWork 调用了一个包含单例缓存管理器的进程,在我的例子中,它正在关闭并从系统进行身份验证并返回一个令牌。然后将此令牌与唯一密钥(用户名)一起存储。现在每个调用,在我的例子中,有 8 个内核/线程,用户名是相同的,比如说“BobUser:CacheKey”。

每次我运行代码时,我都会看到正在发出 8 个请求,因为缓存 Get 总是返回 null。

var token = _cm.Cache.Get<MyToken>(userId);
if (token != null) return token;
token = base.Logon(userId, password);
if (token != null)
{
    _cm.Cache.Add(userId, token);
}
return token; 

这真的是因为 8 个线程同时交互吗?如果是这种情况,是否有解决此并发问题的模式?

谢谢你, 斯蒂芬

【问题讨论】:

    标签: c# multithreading concurrentdictionary


    【解决方案1】:

    这是因为为了启动缓存,至少有一个线程必须完全完成身份验证并添加到缓存中,然后其他线程才能到达顶部的 .Get() 调用。

    var token = _cm.Cache.Get<MyToken>(userId); // <-------------------------------+
    if (token != null) return token;            //                                 |
    token = base.Logon(userId, password);       //                                 |
    if (token != null)                          //                                 |
    {                                           //                                 |
        _cm.Cache.Add(userId, token); //<-- A thread needs to execute this before  |
                                      // other threads even execute this  ----------
    }
    return token; 
    

    阻塞线程并同时启动它们会使事情变得更糟。如果您想修复它,请在您的身份验证周围加上lock,以便一次只发出一个身份验证请求。

    一个示例可能如下所示:

    private static object AuthenticationLocker = new object();
    private string GetToken(string userId)
    {
        lock (AuthenticationLocker)
        {
            var token = _cm.Cache.Get<MyToken>(userId);
            if (token != null) return token;
            token = base.Logon(userId, password);
            if (token != null)
            {
                _cm.Cache.Add(userId, token);
            }
            return token;
        }
    }
    

    请注意,如果您确实开始使用锁,则可以使用常规缓存,而不是线程安全缓存。

    如果您不想使用锁,那么您需要接受这样一个事实,即您可能有多个请求同时调度。不能两全其美

    【讨论】:

    • 我故意加剧了多线程测试工具的问题。此代码将存在于 ASP.NET/WCF 主机中,它“应该”执行得足够好。我在测试期间的解决方案是在手头使用一个没有争用的请求来准备测试,从而使其一直通过。
    【解决方案2】:

    看看So how does ConcurrentDictionary do better?部分

    读取没有锁,所以如果它们都在数据实际存在之前尝试读取,它们都会收到 null。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-20
      • 1970-01-01
      • 1970-01-01
      • 2016-12-28
      • 2021-09-13
      相关资源
      最近更新 更多