【发布时间】:2011-04-18 23:30:50
【问题描述】:
更新:正如 Brian 指出的,我最初的想法确实存在并发问题。 ConcurrentDictionary<TKey, TValue>.AddOrUpdate 方法的签名有点模糊了这一点,它可以让懒惰的思想家(像我自己)相信一切——集合添加和队列推送——都会以某种方式同时发生,原子地(即,神奇地)。
回想起来,抱有这种期望是愚蠢的。事实上,不管AddOrUpdate 的实现如何,应该清楚我最初的想法中仍然存在竞争条件,正如Brian 指出的那样:推入队列会在添加到集合之前发生,因此以下序列可能发生的事件:
- 项目推送到队列
- 项目从队列中弹出
- 项目(未)从集合中移除
- 项目已添加到集合中
上述顺序会导致集合中的项目不在队列中,从而有效地将该项从数据结构中列入黑名单。
现在,我想了一会儿,我开始认为以下方法可以解决这些问题:
public bool Enqueue(T item)
{
// This should:
// 1. return true only when the item is first added to the set
// 2. subsequently return false as long as the item is in the set;
// and it will not be removed until after it's popped
if (_set.TryAdd(item, true))
{
_queue.Enqueue(item);
return true;
}
return false;
}
以这种方式构造它,Enqueue 调用只发生一次 -- 在项目在集合中。所以队列中的重复项应该不是问题。而且似乎由于队列操作被集合操作“预定”了——即,一个项目仅在之后被推送到它被添加到集合中,并且它在之前被弹出它已从集合中移除 - 不应发生上述有问题的事件序列。
人们怎么看?难道这可以解决问题吗? (就像布赖恩一样,我倾向于怀疑自己并猜测答案是否否,我又错过了一些东西。但是,嘿,如果它很容易,那将不是一个有趣的挑战,对吧?)
我确实在 SO 上看到过类似的问题,但令人惊讶的是(考虑到这个网站是多么依赖 .NET),它们似乎都是针对 Java 的。
我基本上需要一个线程安全的集合/队列组合类。换句话说,它应该是一个不允许重复的 FIFO 集合(所以如果同一个项目已经在队列中,后续的Enqueue 调用将返回 false,直到该项目从队列中弹出)。
我意识到我可以通过简单的HashSet<T> 和Queue<T> 很容易地实现这一点,并锁定所有必要的位置。但是,我有兴趣使用 .NET 4.0 中的 ConcurrentDictionary<TKey, TValue> 和 ConcurrentQueue<T> 类(也可作为 .NET 3.5 的 Rx 扩展的一部分,这是我正在使用的)来完成它,我知道这在某种程度上是无锁集合*。
我的基本计划是像这样实现这个集合:
class ConcurrentSetQueue<T>
{
ConcurrentQueue<T> _queue;
ConcurrentDictionary<T, bool> _set;
public ConcurrentSetQueue(IEqualityComparer<T> comparer)
{
_queue = new ConcurrentQueue<T>();
_set = new ConcurrentDictionary<T, bool>(comparer);
}
public bool Enqueue(T item)
{
// This should:
// 1. if the key is not present, enqueue the item and return true
// 2. if the key is already present, do nothing and return false
return _set.AddOrUpdate(item, EnqueueFirst, EnqueueSecond);
}
private bool EnqueueFirst(T item)
{
_queue.Enqueue(item);
return true;
}
private bool EnqueueSecond(T item, bool dummyFlag)
{
return false;
}
public bool TryDequeue(out T item)
{
if (_queue.TryDequeue(out item))
{
// Another thread could come along here, attempt to enqueue, and
// fail; however, this seems like an acceptable scenario since the
// item shouldn't really be considered "popped" until it's been
// removed from both the queue and the dictionary.
bool flag;
_set.TryRemove(item, out flag);
return true;
}
return false;
}
}
我是否考虑得当?从表面上看,我在上面写的这个基本想法中看不到任何明显的错误。但也许我忽略了一些东西。或者也许使用ConcurrentQueue<T> 和ConcurrentDictionary<T, bool> 实际上并不是一个明智的选择,原因我没有想到。或者也许其他人已经在某处经过实战验证的库中实现了这个想法,我应该使用它。
任何关于此主题的想法或有用信息将不胜感激!
*这是否严格准确,我不知道;但性能测试向我表明,它们的性能确实优于对许多消费者线程使用锁定的可比较的手动集合。
【问题讨论】:
-
好问题。这是困难的一个。我想知道
AddOrUpdate的行为如何?当我有时间时,我可能不得不在 Reflector 中打开它。我很想看到这个问题的一些高质量答案。
标签: .net multithreading concurrency queue set