我试图以支持多线程和并发的方式转换现有流程,以使解决方案更加健壮和可靠。
以紧急警报系统为例。当一个工作人员进入时,会创建一个新的Recipient对象及其信息,并将其添加到Recipients集合中。相反,当它们超时时,对象被移除。在后台,当发生警报时,警报引擎将遍历相同的收件人列表(foreach),在每个对象上调用SendAlert(...)。
以下是我的一些要求:
我一直在查看Task和Parallel类以及BlockingCollection和ConcurrentQueue类,但不清楚最好的方法是什么。
是否像使用BlockingCollection一样简单?在阅读了大量文档之后,我仍然不确定如果在枚举集合时调用Add会发生什么。
更新
一位同事向我介绍了以下文章,该文章描述了ConcurrentBag类以及每个操作的行为:
http://www.codethinked.com/net-40-and-system_collections_concurrent_concurrentbag
根据作者的解释,这个集合似乎(几乎)将满足我的目的。我可以做到以下几点:
创建新集合
var recipients = new ConcurrentBag();
当工作人员进入时,创建一个新的收件人并将其添加到集合中:
recipients.Add(new Recipient());
发生警报时,警报引擎可以在此时迭代集合,因为GetEnumerator使用集合项的快照。
foreach(收件人中的var收件人) recipient.SendAlert(...);
当工作人员退出时,从集合中删除收件人:
???
ConcurrentBag没有提供删除特定项目的方法。据我所知,并发类都没有。我错过了什么吗?除此之外,ConcurrentBag还能做我需要的一切。
答案 0 :(得分:1)
今天早上我开始上班,给朋友发来一封电子邮件给我以下两个答案:
1 - 关于Concurrent命名空间中的集合如何工作,其中大部分都设计为允许集合中的添加和减少而不会阻塞,即使在枚举集合项的过程中也是线程安全的。
使用“常规”集合,获取枚举器(通过GetEnumerator)设置“版本”值,该值由影响集合项的任何操作(例如“添加”,“删除”或“清除”)更改。 IEnumerator实现将比较创建时的版本集与当前版本的集合。如果不同,则抛出异常并且枚举停止。
Concurrent集合使用段来设计,使得支持多线程变得非常容易。但是,在枚举的情况下,它们实际上在调用GetEnumerator时创建集合的快照副本,并且枚举器对此副本起作用。这允许对集合进行更改,而不会对枚举器产生不利影响。当然这意味着枚举不知道这些变化,但听起来像你的用例允许这样做。
2 - 就您所描述的特定情况而言,我不认为需要并发集合。您可以使用ReaderWriterLock包装标准集合,并在需要枚举时应用与Concurrent集合相同的逻辑。
这是我的建议:
public class RecipientCollection
{
private Collection<Recipient> _recipients = new Collection<Recipient>();
private ReaderWriterLock _lock = new ReaderWriterLock();
public void Add(Recipient r)
{
_lock.AcquireWriterLock(Timeout.Infinite);
try
{
_recipients.Add(r);
}
finally
{
_lock.ReleaseWriterLock();
}
}
public void Remove(Recipient r)
{
_lock.AcquireWriterLock(Timeout.Infinite);
try
{
_recipients.Remove(r);
}
finally
{
_lock.ReleaseWriterLock();
}
}
public IEnumerable<Recipient> ToEnumerable()
{
_lock.AcquireReaderLock(Timeout.Infinite);
try
{
var list = _recipients.ToArray();
return list;
}
finally
{
_lock.ReleaseReaderLock();
}
}
}
ReaderWriterLock确保仅在另一个更改集合内容的操作正在进行时才会阻止操作。一旦该操作完成,锁定就会被释放,下一个操作就可以继续。
您的警报引擎将使用ToEnumerable()方法获取此时集合的快照副本并枚举副本。
根据发送警报的频率和对集合所做的更改,这可能是一个问题,但您可能仍然可以实现在添加或删除项目时更改的某种类型的版本属性以及警报引擎可以检查此属性以查看是否需要再次调用ToEnumerable()以获取最新版本。或者通过在RecipientCollection类中缓存数组并在添加或删除项目时使缓存无效来封装它。
HTH
答案 1 :(得分:1)
ConcurrentBag<T>
绝对应该是表现最好的一类,供你用于这种情况。枚举与您的朋友描述完全一样,因此它应该适用于您已经布置的场景。但是,知道您必须从此集合中删除特定项目,唯一适合您的类型是ConcurrentDictionary<K, V>
。所有其他类型仅提供TryTake
方法,在ConcurrentBag<T>
的情况下,该方法是不确定的,或仅在ConcurrentQueue<T>
或ConcurrentStack<T>
订购的情况下。
对于广播你只会这样做:
ConcurrentDictionary<string, Recipient> myConcurrentDictionary = ...;
...
foreach(Recipient recipient in myConcurrentDictionary.Values)
{
...
}
在那一刻,枚举器再次成为字典的快照。
答案 2 :(得分:0)
除了并行处理方面之外,这样的实现还有很多,持久性可能在它们之间是最重要的。您是否考虑使用现有的PubSub技术构建此项目,例如说... Azure Topics或NServiceBus?
答案 3 :(得分:-1)
您的要求使我非常适合在C#中触发标准.NET事件的方式。我不知道VB语法是否被编译成类似的代码。标准模式类似于:
public event EventHandler Triggered;
protected void OnTriggered()
{
//capture the list so that you don't see changes while the
//event is being dispatched.
EventHandler h = Triggered;
if (h != null)
h(this, EventArgs.Empty);
}
或者,您可以使用不可变列表类来存储收件人。然后,当发送警报时,它将首先获取当前列表并将其用作&#34;快照&#34;在发送警报时通过添加和删除无法修改。例如:
class Alerter
{
private ImmutableList<Recipient> recipients;
public void Add(Recipient recipient)
{
recipients = recipients.Add(recipient);
}
public void Remove(Recipient recipient)
{
recipients = recipients.Remove(recipient);
}
public void SendAlert()
{
//make a local reference to the current list so
//you are not affected by any calls to Add/Remove
var current = recipients;
foreach (var r in current)
{
//send alert to r
}
}
}
你必须找到一个ImmutableList的实现,但你应该能够找到几个没有太多工作。在我编写的SendAlert
方法中,我可能不需要创建一个显式的局部来避免问题,因为foreach循环本身会这样做,但我认为副本使意图更清晰。 / p>