更新:正如Brian指出的那样,我最初的想法确实存在并发问题。这有点被ConcurrentDictionary<TKey, TValue>.AddOrUpdate
方法的签名所掩盖,它可以让一个懒惰的思想家(比如我自己)相信所有东西 - 集合添加以及队列推送 - 会以某种方式同时发生,原子地(即神奇地)。
回想起来,有这种期望对我来说是愚蠢的。实际上,无论AddOrUpdate
的实现如何,都应该很清楚,在我最初的想法中仍然存在竞争条件,正如布莱恩所指出的那样:在添加到集合之前推送到队列,因此以下可能发生一系列事件:
上述序列会导致集合中的项目不在队列中,从而有效地将该项目列入数据结构中。
现在,我想了一会儿,我开始认为以下方法可以解决这些问题:
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
调用只发生一次 - 在项目在集合中之后。因此,队列中的重复项应该不是问题。而且似乎由于队列操作被设置操作“预订” - 即,只有在之后将项目添加到集合中,并且在之前弹出它被从集合中移除 - 上面列出的有问题的事件序列不应该发生。
人们怎么想?难道这可以解决这个问题吗? (就像Brian一样,我倾向于怀疑自己,并且猜测答案是 No ,我再次错过了一些东西。但是,如果它很容易,那就不会是一个有趣的挑战,对吧?)
我确实在SO上看到了类似的问题,但令人惊讶的是(考虑到.NET网站的重量程度如何),它们似乎都是针对Java的。
我本质上需要一个线程安全的set / queue组合类。换句话说,它应该是一个不允许重复的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>
的{{1}}实际上并不是明智的选择,原因是我没有想到。或许其他人已经在某个经过实战验证的图书馆中实现了这个想法,我应该使用它。
非常感谢有关此主题的任何想法或有用信息!
*这是否严格准确,我不知道;但性能测试向我表明,它们的表现优于使用锁定许多消费者线程的可比手工收藏。
答案 0 :(得分:5)
简称是否定,问题中提供的代码不是线程安全的。
MSDN文档在AddOrUpdate
方法上相当稀疏,所以我看了一下Reflector中的AddOrUpdate
方法。这是基本算法(由于法律原因,我不会发布Reflector输出,并且很容易自己做)。
TValue value;
do
{
if (!TryGetValue(...))
{
value = AddValueFactoryDelegate(key);
if (!TryAddInternal(...))
{
continue;
}
return value;
}
value = UpdateValueFactoryDelegate(key);
}
while (!TryUpdate(...))
return value;
很明显AddValueFactoryDelegate
和UpdateValueFactoryDelegate
可以执行多次。这里不需要进一步解释。应该很明显这会如何破坏你的代码。我实际上有点震惊,代表们可以多次执行。该文件没有提到这一点。您会认为这将是一个非常重要的观点,因此呼叫者知道避免传递具有副作用的代表(就像您的情况一样)。
但即使代表们只保留执行一次仍然存在问题。通过将Enqueue
方法替换为AddOrUpdate
方法的内容,可以很容易地将问题序列可视化。 AddValueFactoryDelegate
可以执行并将项插入_queue
,但在将项添加到_set
之前,线程可能会被上下文切换中断。然后,第二个帖子可以调用您的TryDequeue
方法,并从_queue
中提取该项,但无法将其从_set
中删除,因为它尚未存在。
<强>更新强>
好吧,我认为不可能让它发挥作用。 ConcurrentQueue
缺少一项关键操作。我相信您需要TryDequeue
等效TryDequeueCas
方法。如果存在这样的操作,那么我认为以下代码是正确的。我使用神秘的Interlocked.CompareExchange
方法接受一个比较值,该值用作条件,以便当且仅当队列中的顶部项等于比较值时才原子地执行此操作。这个想法与bool
方法中使用的完全相同。
注意代码如何使用ConcurrentDictionary
中的TryUpdate
值作为“虚拟”锁来同步队列和字典的协调。数据结构还包含CAS等效操作while
,用于获取和释放此“虚拟”锁。并且因为锁是“虚拟的”并且实际上不阻止并发访问,所以TryDequeue
方法中的ConcurrentQueue.TryDequeueCas
循环是强制性的。这符合CAS操作的规范模式,因为它们通常在循环中执行,直到它们成功。
代码还使用CAS来获取锁定获取语义,以帮助防止由带外(异步)异常引起的问题。
注意:同样,代码使用神秘的class ConcurrentSetQueue<T>
{
ConcurrentQueue<T> _queue = new ConcurrentQueue<T>();
ConcurrentDictionary<T, bool> _set = new ConcurrentDictionary<T, bool>();
public ConcurrentSetQueue()
{
}
public bool Enqueue(T item)
{
bool acquired = false;
try
{
acquired = _set.TryAdd(item, true);
if (acquired)
{
_queue.Enqueue(item);
return true;
}
return false;
}
finally
{
if (acquired) _set.TryUpdate(item, false, true);
}
}
public bool TryDequeue(out T item)
{
while (_queue.TryPeek(out item))
{
bool acquired = false;
try
{
acquired = _set.TryUpdate(item, true, false);
if (acquired)
{
if (_queue.TryDequeueCas(out item, item))
{
return true;
}
}
}
finally
{
if (acquired) _set.TryRemove(item, out acquired);
}
}
item = default(T);
return false;
}
}
方法。
Enqueue
更新2:
参考您的修改通知,与我的相比,它是多么相似。事实上,如果您从我的变体中删除了所有的绒毛,TryDequeue
方法的完全相同的语句序列。
我更担心Enqueue
这就是为什么我添加了“虚拟”锁定概念,这在我的实现中需要很多额外的东西。我特别担心访问数据结构的相反顺序(字典然后在TryDequeue
方法中排队,但在{{1}}方法中排队然后字典)但是,我对你修改过的方法的考虑越多我越喜欢它。我现在认为它是因为反向访问顺序的是安全的!
答案 1 :(得分:1)
看看Eric Lppert的博客。你可能会发现你喜欢的东西...... http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/