[英]Generic list concurrent access - clear part of list while data is getting stored
我有一个通用的List<T>
,其中存储了来自网络套接字的实时流数据。 我想将通用列表中的数据存储到数据库并清除列表,以便可以存储新的流数据而不会填满我的机器内存。
如果我枚举列表以将数据发送到数据库,我会遇到异常,因为在我尝试枚举或清除列表时数据正在添加到列表中。 如果我在列表上应用锁定,流数据将暂停,这是不允许的。
请建议我如何解决这个问题。
似乎是BatchBlock的工作
它是完全线程安全的,非常适合数据流。 DataFlow.Net 库中有很多类,但适合您情况的是BatchBlock
。
BatchBlock
收集数据,直到达到大小阈值。 遇之,则整批为结果。 您可以通过不同的方式获得结果,例如.Receive
或ReceiveAll
或它们的异步方式。 另一种方法是将批处理结果链接到另一个块,如ActionBlock
,每次从源块(在本例中为 BatchBlock)提供输入时,它将异步调用提供的Action
,因此基本上每次批处理满时都会发送到动作块。 ActionBlock
可以接收像MaxDegreeOfParallelism
这样的参数来避免数据库锁定或 smth 如果你需要的话,但它不会以任何方式阻塞BatchBlock
所以不需要在客户端等待,批次将简单地放在一个队列中(线程安全) ActionBlock
执行的动作块。
不用担心,当批次变满时,它也不会停止接收新项目,因此不会再次阻塞。 一个漂亮的解决方案。
需要担心的一件事是,如果批处理没有达到完整大小,但您停止了应用程序,结果将会丢失,因此您可以手动TriggerBatch
将与批处理中一样多的项目发送到ActionBlock
。 因此,您可以在Dispose
或 smth 中调用TriggerBatch
,由您决定。
在BatchBlock
中也有两种输入项目的方法: Post
和SendAsync
。 我相信Post
正在阻塞(尽管我不确定),但是如果BatchBlock
繁忙, SendAsync
会推迟消息。
class ConcurrentCache<T> : IAsyncDisposable {
private readonly BatchBlock<T> _batchBlock;
private readonly ActionBlock<T[]> _actionBlock;
private readonly IDisposable _linkedBlock;
public ConcurrentCache(int cacheSize) {
_batchBlock = new BatchBlock<T>(cacheSize);
// action to do when the batch max capacity is met
// the action can be an async task
_actionBlock = new ActionBlock<T[]>(ReadBatchBlock);
_linkedBlock = _batchBlock.LinkTo(_actionBlock);
}
public async Task SendAsync(T item) {
await _batchBlock.SendAsync(item);
}
private void ReadBatchBlock(T[] items) {
foreach (var item in items) {
Console.WriteLine(item);
}
}
public async ValueTask DisposeAsync() {
_batchBlock.Complete();
await _batchBlock.Completion;
_batchBlock.TriggerBatch();
_actionBlock.Complete();
await _actionBlock.Completion;
_linkedBlock.Dispose();
}
}
使用示例:
await using var cache = new ConcurrentCache<int>(5);
for (int i = 0; i < 12; i++) {
await cache.SendAsync(i);
await Task.Delay(200);
}
当对象将被处置时,将触发并打印剩余的批次。
更新
正如@TheodorZoulias 指出的那样,如果批次未填满且长时间没有消息,则消息将卡在 BatchBlock 中。 解决方案是创建一个计时器来调用.TriggerBatch()
。
如果我在列表上应用锁定,流数据将暂停,这是不允许的
你应该只持有锁尽可能短的时间。 在这种情况下,应该是从列表中添加或删除一个项目。 在将数据添加到数据库或任何其他缓慢的操作时,您不应该持有锁。 获得一个无竞争的锁大约需要 25ns ,这应该只在非常紧凑的循环中才会出现问题。
但更好的选择是使用内置的线程安全集合,如BlockingCollection 。 后者非常方便,因为它具有GetConsumingEnumerable
和CompleteAdding
等方法。 这让您的消费者只需使用常规的 foreach 循环来消费项目,而生产者只需调用 CompleteAdding 让循环在处理完所有项目后退出。
您可能还想看看DataFlow 。 我自己没有使用过它,但它似乎适合设置并发处理管道。
然而,在尝试进行任何类型的并发处理之前,您需要相当熟悉线程安全和相关的危险。 线程安全很难,你需要知道什么是安全的,什么是不安全的。 当你搞砸时,你不会总是幸运地得到异常,你可能只是丢失或不正确的数据。
我认为你应该尝试Parallel.ForEach和ConcurrentDictionary
var streamingDataList = new ConcurrentDictionary<int, StreamingDataModel>();
Parallel.ForEach(streamingDataBatch, streamingData =>
{
streamingDataList.TryAdd(streamingData.Id,streamingData.Data));
});
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.