繁体   English   中英

通用列表并发访问 - 在存储数据时清除列表的一部分

[英]Generic list concurrent access - clear part of list while data is getting stored

我有一个通用的List<T> ,其中存储了来自网络套接字的实时流数据。 我想将通用列表中的数据存储到数据库并清除列表,以便可以存储新的流数据而不会填满我的机器内存。

如果我枚举列表以将数据发送到数据库,我会遇到异常,因为在我尝试枚举或清除列表时数据正在添加到列表中。 如果我在列表上应用锁定,流数据将暂停,这是不允许的。

请建议我如何解决这个问题。

似乎是BatchBlock的工作

它是完全线程安全的,非常适合数据流。 DataFlow.Net 库中有很多类,但适合您情况的是BatchBlock

BatchBlock收集数据,直到达到大小阈值。 遇之,则整批为结果。 您可以通过不同的方式获得结果,例如.ReceiveReceiveAll或它们的异步方式。 另一种方法是将批处理结果链接到另一个块,如ActionBlock ,每次从源块(在本例中为 BatchBlock)提供输入时,它将异步调用提供的Action ,因此基本上每次批处理满时都会发送到动作块。 ActionBlock可以接收像MaxDegreeOfParallelism这样的参数来避免数据库锁定或 smth 如果你需要的话,但它不会以任何方式阻塞BatchBlock所以不需要在客户端等待,批次将简单地放在一个队列中(线程安全) ActionBlock执行的动作块。

不用担心,当批次变满时,它也不会停止接收新项目,因此不会再次阻塞。 一个漂亮的解决方案。

需要担心的一件事是,如果批处理没有达到完整大小,但您停止了应用程序,结果将会丢失,因此您可以手动TriggerBatch将与批处理中一样多的项目发送到ActionBlock 因此,您可以在Dispose或 smth 中调用TriggerBatch ,由您决定。

BatchBlock中也有两种输入项目的方法: PostSendAsync 我相信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 后者非常方便,因为它具有GetConsumingEnumerableCompleteAdding等方法。 这让您的消费者只需使用常规的 foreach 循环来消费项目,而生产者只需调用 CompleteAdding 让循环在处理完所有项目后退出。

您可能还想看看DataFlow 我自己没有使用过它,但它似乎适合设置并发处理管道。

然而,在尝试进行任何类型的并发处理之前,您需要相当熟悉线程安全和相关的危险。 线程安全很难,你需要知道什么是安全的,什么是不安全的。 当你搞砸时,你不会总是幸运地得到异常,你可能只是丢失或不正确的数据。

我认为你应该尝试Parallel.ForEachConcurrentDictionary

var streamingDataList = new ConcurrentDictionary<int, StreamingDataModel>();
Parallel.ForEach(streamingDataBatch, streamingData =>
{                            
  streamingDataList.TryAdd(streamingData.Id,streamingData.Data));
});

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM