为什么在使用多个并发线程时从 ConcurrentDictionary 返回的值总是 null?
Why is the value returned from a ConcurrentDictionary always null when multiple concurrent threads are used?
我实现了一个由 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 调用一个包含单例缓存管理器的进程,在我的例子中,它正在运行并从系统和 returns 令牌进行身份验证。然后,此令牌与唯一密钥(用户名)一起存储。现在这些调用中的每一个,在我的例子中有 8 个 cores/threads 用户名是相同的,比方说 "BobUser:CacheKey".
每次我 运行 我看到正在发出 8 个请求的代码,因为缓存总是 returns 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个线程恰好同时交互的缘故吗?如果是这样,是否有解决此并发问题的模式?
谢谢,
斯蒂芬
发生这种情况是因为为了启动缓存,至少有一个线程必须完全完成身份验证并添加到缓存,然后其他线程才能到达顶部的 .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;
}
}
请注意,如果您确实开始使用锁,则可以使用常规缓存,而不是线程安全缓存。
如果您不想使用锁,那么您需要接受这样一个事实,即您可能会同时发送多个请求。你不能两全其美
看看So how does ConcurrentDictionary do better?
部分
读取没有锁,所以如果它们都在数据实际存在之前尝试读取,它们都会收到 null。
我实现了一个由 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 调用一个包含单例缓存管理器的进程,在我的例子中,它正在运行并从系统和 returns 令牌进行身份验证。然后,此令牌与唯一密钥(用户名)一起存储。现在这些调用中的每一个,在我的例子中有 8 个 cores/threads 用户名是相同的,比方说 "BobUser:CacheKey".
每次我 运行 我看到正在发出 8 个请求的代码,因为缓存总是 returns 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个线程恰好同时交互的缘故吗?如果是这样,是否有解决此并发问题的模式?
谢谢, 斯蒂芬
发生这种情况是因为为了启动缓存,至少有一个线程必须完全完成身份验证并添加到缓存,然后其他线程才能到达顶部的 .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;
}
}
请注意,如果您确实开始使用锁,则可以使用常规缓存,而不是线程安全缓存。
如果您不想使用锁,那么您需要接受这样一个事实,即您可能会同时发送多个请求。你不能两全其美
看看So how does ConcurrentDictionary do better?
部分读取没有锁,所以如果它们都在数据实际存在之前尝试读取,它们都会收到 null。