C#可中止异步Fifo队列-泄漏大量内存

C#可中止异步Fifo队列-泄漏大量内存,c#,memory-leaks,async-await,concurrent-queue,C#,Memory Leaks,Async Await,Concurrent Queue,我需要以FIFO方式处理来自生产者的数据,如果同一生产者产生新的数据位,则能够中止处理 因此,我基于Stephen Cleary的AsyncCollection(在我的示例中称为AsyncCollectionAbortableFifoQueue)和TPL的BufferBlock(在我的示例中称为BufferBlockAbortableAsyncFifoQueue)实现了一个可中止FIFO队列。下面是基于AsyncCollection public class AsyncCollectionAbo

我需要以FIFO方式处理来自生产者的数据,如果同一生产者产生新的数据位,则能够中止处理

因此,我基于Stephen Cleary的
AsyncCollection
(在我的示例中称为
AsyncCollectionAbortableFifoQueue
)和TPL的
BufferBlock
(在我的示例中称为
BufferBlockAbortableAsyncFifoQueue
)实现了一个可中止FIFO队列。下面是基于
AsyncCollection

public class AsyncCollectionAbortableFifoQueue<T> : IExecutableAsyncFifoQueue<T>
{
    private AsyncCollection<AsyncWorkItem<T>> taskQueue = new AsyncCollection<AsyncWorkItem<T>>();
    private readonly CancellationToken stopProcessingToken;

    public AsyncCollectionAbortableFifoQueue(CancellationToken cancelToken)
    {
        stopProcessingToken = cancelToken;
        _ = processQueuedItems();
    }

    public Task<T> EnqueueTask(Func<Task<T>> action, CancellationToken? cancelToken)
    {
        var tcs = new TaskCompletionSource<T>();
        var item = new AsyncWorkItem<T>(tcs, action, cancelToken);
        taskQueue.Add(item);
        return tcs.Task;
    }

    protected virtual async Task processQueuedItems()
    {
        while (!stopProcessingToken.IsCancellationRequested)
        {
            try
            {
                var item = await taskQueue.TakeAsync(stopProcessingToken).ConfigureAwait(false);
                if (item.CancelToken.HasValue && item.CancelToken.Value.IsCancellationRequested)
                    item.TaskSource.SetCanceled();
                else
                {
                    try
                    {
                        T result = await item.Action().ConfigureAwait(false);
                        item.TaskSource.SetResult(result);   // Indicate completion
                    }
                    catch (Exception ex)
                    {
                        if (ex is OperationCanceledException && ((OperationCanceledException)ex).CancellationToken == item.CancelToken)
                            item.TaskSource.SetCanceled();
                        item.TaskSource.SetException(ex);
                    }
                }
            }
            catch (Exception) { }
        }
    }
}

public interface IExecutableAsyncFifoQueue<T>
{
    Task<T> EnqueueTask(Func<Task<T>> action, CancellationToken? cancelToken);
}
然后会有一个任务查找要处理的项目并将其出列,或者处理它们,或者在触发
CancellationToken
时中止

所有工作正常-数据得到处理,如果接收到新的数据,旧数据的处理将中止。我现在的问题是,如果我提高使用率,这些队列会泄漏大量内存(生产者生产的内存远远多于消费者生产的内存)。如果数据是可中止的,则未经处理的数据应被丢弃,并最终从内存中消失

让我们看看我是如何使用这些队列的。我有一个1:1的生产者和消费者匹配。每个消费者处理单个生产者的数据。每当我得到一个新的数据项,而它与前一个数据项不匹配时,我就会捕获给定生产者(User.UserId)的队列,或者创建一个新的队列(代码段中的“executor”)。然后我有一个
ConcurrentDictionary
,它保存了每个生产者/消费者组合的
CancellationTokenSource
。如果有以前的
cancelationtokensource
,我在上面调用
Cancel
并在20秒后调用
Dispose(立即处理会导致队列中的异常)。然后,我将新数据的处理排队。队列返回一个我可以等待的任务,以便我知道数据处理何时完成,然后返回结果

这是代码

internal class SimpleLeakyConsumer
{
    private ConcurrentDictionary<string, IExecutableAsyncFifoQueue<bool>> groupStateChangeExecutors = new ConcurrentDictionary<string, IExecutableAsyncFifoQueue<bool>>();
    private readonly ConcurrentDictionary<string, CancellationTokenSource> userStateChangeAborters = new ConcurrentDictionary<string, CancellationTokenSource>();
    protected CancellationTokenSource serverShutDownSource;
    private readonly int operationDuration = 1000;

    internal SimpleLeakyConsumer(CancellationTokenSource serverShutDownSource, int operationDuration)
    {
        this.serverShutDownSource = serverShutDownSource;
        this.operationDuration = operationDuration * 1000; // convert from seconds to milliseconds
    }

    internal async Task<bool> ProcessStateChange(string userId)
    {
        var executor = groupStateChangeExecutors.GetOrAdd(userId, new AsyncCollectionAbortableFifoQueue<bool>(serverShutDownSource.Token));
        CancellationTokenSource oldSource = null;
        using (var cancelSource = userStateChangeAborters.AddOrUpdate(userId, new CancellationTokenSource(), (key, existingValue) =>
        {
            oldSource = existingValue;
            return new CancellationTokenSource();
        }))
        {
            if (oldSource != null && !oldSource.IsCancellationRequested)
            {
                oldSource.Cancel();
                _ = delayedDispose(oldSource);
            }
            try
            {
                var executionTask = executor.EnqueueTask(async () => { await Task.Delay(operationDuration, cancelSource.Token).ConfigureAwait(false); return true; }, cancelSource.Token);
                var result = await executionTask.ConfigureAwait(false);
                userStateChangeAborters.TryRemove(userId, out var aborter);
                return result;
            }
            catch (Exception e)
            {
                if (e is TaskCanceledException || e is OperationCanceledException)
                    return true;
                else
                {
                    userStateChangeAborters.TryRemove(userId, out var aborter);
                    return false;
                }
            }
        }
    }

    private async Task delayedDispose(CancellationTokenSource src)
    {
        try
        {
            await Task.Delay(20 * 1000).ConfigureAwait(false);
        }
        finally
        {
            try
            {
                src.Dispose();
            }
            catch (ObjectDisposedException) { }
        }
    }
}
因此,如果我运行我的测试仪并键入“start”,则
Producer
类开始生成
使用者所使用的状态。内存使用量开始不断增长。示例配置到了极限,我正在处理的实际场景强度较小,但生产者的一个操作可能会触发消费者端的多个操作,这些操作也必须以相同的异步可中止fifo方式执行-因此,最坏的情况是,产生的一组数据会触发约10个消费者的操作(为了简洁起见,我删去了最后一部分)

当我有100个生产者时,每个生产者每1-6秒产生一个新的数据项(随机,数据产生也是随机的)。消耗数据需要3秒。因此,在很多情况下,在旧数据被正确处理之前,会有一组新数据

查看两个连续的内存转储,很明显内存使用来自何处。所有的片段都与队列有关。考虑到我正在处理每个TaskCancellationSource,并且没有保留对生成的数据的任何引用(以及它们放入的
AsyncWorkItem
),我无法解释为什么这会一直占用我的内存,我希望其他人能告诉我我的错误。你也可以通过键入“停止”来中止测试……你会看到内存不再被占用,但即使你暂停并触发GC,内存也不会被释放


可运行表单中项目的源代码已打开。启动后,您必须在控制台中键入
start
(加上enter)以通知制作者开始生成数据。您可以通过键入
stop
(加上enter)停止生成数据

您的代码有太多问题,无法通过调试找到漏洞。但以下几点已经是一个问题,应该首先解决:

每次调用
processUseStateUpdateAsync
时,类似于
getQueue
为同一用户创建一个新队列,并且不重用现有队列:

var executor = groupStateChangeExecutors.GetOrAdd(user.UserId, getQueue());
CancellationTokenSource
在每次调用下面的代码时都会泄漏,因为每次调用方法
AddOrUpdate
时都会创建新值,因此不应以这种方式将其传递到那里:

userStateChangeAborters.AddOrUpdate(user.UserId, new CancellationTokenSource(), (key, existingValue
此外,如果字典没有特定
用户的值,则下面的代码应使用与新cts相同的cts。UserId

return new CancellationTokenSource();
另外,
cancelSource
变量可能会泄漏,因为它绑定到一个可以比您想要的时间更长的委托,最好将具体的
CancellationToken
传递到那里:

executor.EnqueueTask(() => processUserStateUpdateAsync(user, state, previousState,
                    cancelSource.Token));
由于某种原因,您没有在此处和另一个位置处置
中止程序

userStateChangeAborters.TryRemove(user.UserId, out var aborter);
创建
通道
可能存在潜在泄漏:

taskQueue = Channel.CreateBounded<AsyncWorkItem<T>>(new BoundedChannelOptions(1)
taskQueue=Channel.CreateBounded(新的BoundedChannelOptions(1)
您选择了选项
FullMode=BoundedChannelFullMode.DropOldest
,如果存在最早的值,则该选项应删除最早的值,因此我假设这会停止处理队列中的项目,因为它们不会被读取。这是一个假设,但我假设如果旧项目未经处理就被删除,则
processUserStateUpdateAsync
wo不会被调用,也不会释放所有资源


您可以从这些发现的问题开始,之后应该更容易找到真正的原因。

似乎您没有使用
取消令牌来取消正在运行的任务。您使用它们只是为了避免启动尚未创建和启动的任务。因此,您没有充分使用取消机制您可以使用更轻量级的、非一次性的东西,比如
volatile bool
wrappers。您是否使用
CancellationToken
s来选择在将来引入可取消的任务?此外,您似乎需要的是一个异步队列,它可以在其缓冲区中最多容纳一个元素,即d将取代旧的bu
executor.EnqueueTask(() => processUserStateUpdateAsync(user, state, previousState,
                    cancelSource.Token));
userStateChangeAborters.TryRemove(user.UserId, out var aborter);
taskQueue = Channel.CreateBounded<AsyncWorkItem<T>>(new BoundedChannelOptions(1)