.net 这似乎是并发集合/队列组合的合理方法吗?
更新:正如Brian指出的,我最初的想法确实存在并发问题。这在某种程度上被.net 这似乎是并发集合/队列组合的合理方法吗?,.net,multithreading,concurrency,queue,set,.net,Multithreading,Concurrency,Queue,Set,更新:正如Brian指出的,我最初的想法确实存在并发问题。这在某种程度上被ConcurrentDictionary.AddOrUpdate方法的签名所掩盖,该方法可以诱使一个懒惰的思考者(比如我)相信一切——集合添加和队列推送——都会以某种方式同时发生,原子化(即神奇地发生) 回想起来,我有这样的期望是愚蠢的。事实上,无论如何实现AddOrUpdate,我的原始想法中仍然存在竞争条件,正如Brian指出的那样:在添加到集合之前,会发生推送到队列的情况,因此可能会发生以下事件序列: 推送到队列的项
ConcurrentDictionary.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
调用只发生一次——在项目进入集合之后。因此,队列中重复的项目应该不是问题。而且,似乎由于队列操作是由集合操作“bookend”的——也就是说,一个项目只有在添加到集合中之后才被推送,并且在从集合中移除之前被弹出——所以上述有问题的事件序列不应该发生
人们怎么想?这能解决问题吗?(像布赖恩一样,我倾向于怀疑自己,并猜测答案是否定的,我又错过了一些东西。但是,嘿,如果这很容易,那就不是一个有趣的挑战,对吗?)
我在这里肯定看到过类似的问题,但令人惊讶的是(考虑到这个网站有多重的.NET),它们似乎都是针对Java的 我基本上需要一个线程安全的集合/队列组合类。换句话说,它应该是一个不允许重复的FIFO集合(因此,如果相同的项已经在队列中,则后续的
Enqueue
调用将返回false,直到该项从队列中弹出)
我意识到我可以通过一个简单的哈希集
和一个队列
实现这一点,并在所有必要的位置锁定。然而,我对使用.NET 4.0中的和类(也可以作为.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;
}
}
类ConcurrentSetQueue
{
ConcurrentQueue\u队列;
ConcurrentDictionary\u集;
公共ConcurrentSetQueue(IEqualityComparer比较器)
{
_队列=新的ConcurrentQueue();
_set=新的ConcurrentDictionary(比较器);
}
公共布尔排队(T项)
{
//这应:
//1.如果密钥不存在,则将该项排队并返回true
//2.如果密钥已存在,则不执行任何操作并返回false
return _set.AddOrUpdate(item,排队第一,排队第二);
}
私有布尔排队第一(T项)
{
_排队。排队(项目);
返回true;
}
私有布尔排队秒(T项,布尔dummyFlag)
{
返回false;
}
公共bool TryDequeue(out T项)
{
if(_queue.TryDequeue(out项))
{
//另一个线程可能会出现在这里,尝试排队,然后
//失败;但是,自
//项目不应该被认为是真正的“弹出”,直到它被删除
//已从队列和字典中删除。
布尔旗;
_set.TryRemove(项目,输出标志);
返回true;
}
返回false;
}
}
我想清楚了吗?从表面上看,我在上面写的这个基本想法中看不到任何明显的错误。但也许我忽略了什么。或者可能将ConcurrentQueue
与ConcurrentDictionary
一起使用实际上不是一个明智的选择,原因我没有想到。或者其他人已经在某个地方的一个经过战斗验证的库中实现了这个想法,我应该使用它
任何关于这个主题的想法或有用的信息都将不胜感激
*我不知道这是否严格准确;但性能测试向我表明,它们的性能确实优于类似的手动集合,这些集合在许多消费线程中使用锁定。请看Eric Lppert的博客。你可能会找到你喜欢的东西。。。
简短的回答是否,问题中的代码不是线程安全的 MSDN文档中关于
AddOrUpdate
方法的内容非常少,因此我在Reflector中查看了AddOrUpdate
方法。以下是基本算法(出于法律原因,我不会发布反射器输出,这很容易自己完成)
因此,显然,AddValueFactoryDelegate
和UpdateValueFactoryDelegate
可以执行多次。这里没有必要作进一步解释。很明显,这将破坏您的代码。事实上,我有点震惊,代表们可以被执行多次。文件中没有提到这一点。你可能会认为这是一个非常重要的问题,让打电话的人知道避免经过有副作用的代表(就像你的情况一样)
但是,即使保证代理只执行一次,仍然会有问题。通过将Enqueue
方法替换为AddOrUpdate的内容,可以很容易地可视化问题序列<
TValue value;
do
{
if (!TryGetValue(...))
{
value = AddValueFactoryDelegate(key);
if (!TryAddInternal(...))
{
continue;
}
return value;
}
value = UpdateValueFactoryDelegate(key);
}
while (!TryUpdate(...))
return value;
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;
}
}