C# dispose后具有CancellationTokenSource且超时内存泄漏的通道

C# dispose后具有CancellationTokenSource且超时内存泄漏的通道,c#,.net,async-await,memory-leaks,system.threading.channels,C#,.net,Async Await,Memory Leaks,System.threading.channels,完整的可复制代码是,在启动可执行文件后,内存将很快启动。代码主要驻留在AsyncBlockingQueue.cs类中 以下代码实现了一个简单的异步“阻塞”队列: 更新 从评论中: 有一个处理器可以批量处理消息。当有足够的消息或时间到了,它就会开始处理,这就是超时取消的原因 这意味着真正需要的是一种按计数和周期对消息进行批处理的方法。两者都相对容易 此方法按计数进行批处理。该方法将消息添加到批处理列表中,直到达到限制,向下游发送数据并清除列表: static ChannelReader<Me

完整的可复制代码是,在启动可执行文件后,内存将很快启动。代码主要驻留在
AsyncBlockingQueue.cs
类中

以下代码实现了一个简单的异步“阻塞”队列:


更新

从评论中:

有一个处理器可以批量处理消息。当有足够的消息或时间到了,它就会开始处理,这就是超时取消的原因

这意味着真正需要的是一种按计数和周期对消息进行批处理的方法。两者都相对容易

此方法按计数进行批处理。该方法将消息添加到批处理列表中,直到达到限制,向下游发送数据并清除列表:

static ChannelReader<Message[]> BatchByCount(this ChannelReader<Message> input, int count, CancellationToken token=default)
{
    var channel=Channel.CreateUnbounded();
    var writer=channel.Writer;   

    _ = Task.Run(async ()=>{
        var batch=new List<Message>(count);
        await foreach(var msg in input.ReadAllAsync(token))
        {
            batch.Add(msg);
            if(batch.Count==count)
            {
                await writer.WriteAsync(batch.ToArray());
                batch.Clear();
            }
        }
    },token)
   .ContinueWith(t=>writer.TryComplete(t.Exception));
   return channel;
}
两者兼而有之——我仍在努力。问题是计数和计时器过期可能同时发生。最坏的情况是,
lock(batch)
可用于确保只有线程或循环可以向下游发送数据

原始答案


通道在正确使用时不会泄漏-就像任何其他容器一样。通道不是异步队列,也绝对不是阻塞队列。这是一个完全不同的结构,有着完全不同的习语。它是一个使用队列的高级容器。有一个很好的理由,有单独的ChannelReader和ChannelWriter类

典型的情况是由发布者创建并拥有频道。只有发布者才能写入该频道并对其调用
Complete()
<代码>频道未实现
IDisposable
,因此无法对其进行处置。出版商仅向订阅者提供
频道阅读器

订阅者只能看到
频道阅读器
,并从中读取,直到它完成为止。通过使用
ReadAllAsync
,订阅者可以一直从Channel Reader读取,直到完成

这是一个典型的例子:

ChannelReader<Message> Producer(CancellationToken token=default)
{
    var channel=Channel.CreateUnbounded<Message>();
    var writer=channel.Writer;

    //Create the actual "publisher" worker
    _ = Task.Run(async ()=>{
        for(int i=0;i<100;i++)
        {
            //Check for cancellation
            if(token.IsCancellationRequested)
            {
                return;
            }
            //Simulate some work
            await Task.Delay(100);
            await writer.WriteAsync(new Message(...));          
        }
    }  ,token)
    //Complete and propagate any exceptions
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    //This casts to a ChannelReader
    return channel;
}
订户可以通过返回ChannelReader来生成自己的消息。这就是事情变得非常有趣的地方,因为
Subscriber
方法变成了链接步骤管道中的一个步骤。如果我们将方法转换为
ChannelReader
上的扩展方法,我们可以轻松创建整个管道

让我们生成一些数字:

ChannelReader<int> Generate(int nums,CancellationToken token=default)
{
    var channel=Channel.CreateBounded<int>(10);
    var writer=channel.Writer;

    //Create the actual "publisher" worker
    _ = Task.Run(async ()=>{
        for(int i=0;i<nums;i++)
        {
            //Check for cancellation
            if(token.IsCancellationRequested)
            {
                return;
            }

            await writer.WriteAsync(i*7);  
            await Task.Delay(100);        
        }
    }  ,token)
    //Complete and propagate any exceptions
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    //This casts to a ChannelReader
    return channel;
}
并将取消令牌添加到所有步骤:

using var cts=new CancellationTokenSource();
await Generate(100,cts.Token)
          .Double(cts.Token)
          .Square(cts.Token)
          .Print(cts.Token);

如果一个步骤产生的消息比长时间消耗的消息快,内存使用就会增加。这可以通过使用有界通道而不是无界通道轻松处理。这样,如果一个方法太慢,那么在发布新数据之前,所有以前的方法都必须等待。

我能够重现您观察到的问题。这显然是IMHO图书馆的一个缺陷。这是我的报告:

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

public static class Program
{
    public static async Task Main()
    {
        var channel = Channel.CreateUnbounded<int>();
        var bufferBlock = new BufferBlock<int>();
        var asyncCollection = new Nito.AsyncEx.AsyncCollection<int>();
        var mem0 = GC.GetTotalMemory(true);
        int timeouts = 0;
        for (int i = 0; i < 10; i++)
        {
            var stopwatch = Stopwatch.StartNew();
            while (stopwatch.ElapsedMilliseconds < 500)
            {
                using var cts = new CancellationTokenSource(1);
                try
                {
                    await channel.Reader.ReadAsync(cts.Token);
                    //await bufferBlock.ReceiveAsync(cts.Token);
                    //await asyncCollection.TakeAsync(cts.Token);
                }
                catch (OperationCanceledException) { timeouts++; }
            }
            var mem1 = GC.GetTotalMemory(true);
            Console.WriteLine($"{i + 1,2}) Timeouts: {timeouts,5:#,0},"
                + $" Allocated: {mem1 - mem0:#,0} bytes");
        }
    }
}

每次操作都会泄漏大约800字节,这相当令人讨厌。每次在通道中写入新值时,都会回收内存,因此对于忙碌的通道,此设计缺陷不应成为问题。但对于一个偶尔接收值的频道来说,这可能是一个阻碍


还有其他可用的异步队列实现,它们不会遇到相同的问题。您可以尝试注释
wait channel.Reader.ReadAsync(cts.Token)行,并取消对下面两行中任何一行的注释。您将看到,库中的和包中的都允许从队列中进行异步检索,并允许超时,而不会出现内存泄漏。

我太专注于实际问题的技术细节了,我忘了问题已经解决了,几乎是开箱即用

从评论来看,实际问题似乎是:

有一个处理器可以批量处理消息。当有足够的消息或时间到了,它就会开始处理,这就是超时取消的原因

这是由的运营商提供的,该运营商由创建以下内容的同一团队创建:

ToObservable
ToAsyncEnumerable
由ReactiveX.NET使用的接口
IAsyncEnumerable
IObservable
提供并在两者之间进行转换

由提供,并按计数或周期逐项缓冲,甚至允许重叠序列

虽然LINQ和LINQtoAsync在对象上提供查询运算符,但Rx.NET在基于时间的事件流上也提供相同的操作。可以随时间聚合、按时间缓冲事件、限制事件等。文档(非官方)页面中的示例演示了如何创建重叠序列(例如滑动窗口)。同一页显示了如何使用
Sample
Throttle
通过仅传播时段中的最后一个事件来限制快速事件流

Rx使用推送模型(将新事件推送到订阅者),而IAsyncEnumerable(如IEnumerable)使用拉送模型
ToAsyncEnumerable()
将缓存项目,直到请求它们为止,如果没有人在侦听,则可能会导致问题

使用这些方法,甚至可以创建扩展方法来缓冲或限制发布服务器:

    //Returns all items in a period
    public static IAsyncEnumerable<IList<T>> BufferAsync<T>(
        this ChannelReader<T> reader, 
        TimeSpan timeSpan, 
        int count,
        CancellationToken token = default)
    {
        return reader.ReadAllAsync(token)
            .ToObservable()
            .Buffer(timeSpan, count)
            .ToAsyncEnumerable();
    }
        
        
    //Return the latest item in a period
    public static IAsyncEnumerable<T> SampleAsync<T>(
        this ChannelReader<T> reader, 
        TimeSpan timeSpan,
        CancellationToken token = default)
    {
        return reader.ReadAllAsync(token)
            .ToObservable()
            .Sample(timeSpan)
            .ToAsyncEnumerable();
    }
//返回一段时间内的所有项目
公共静态IAsyncEnumerable BufferAsync(
这个通灵读取器,
时间跨度时间跨度,
整数计数,
CancellationToken(默认值)
{
返回reader.ReadAllAsync(令牌)
.TooObservable()文件
.Buffer(时间跨度、计数)
.ToAsyncEnumerable();
}
//返回期间中的最新项目
公共静态IAsyncEnumerable SamPleaseSync(
这个通灵读取器,
时间跨度时间跨度,
CancellationToken(默认值)
{
返回reader.ReadAllAsync(令牌)
.TooObservable()文件
.样本(时间跨度)
.ToAsyncEnumerable();
}

这似乎是一个老生常谈的话题,但它仍然让我感到困惑和泄密
ChannelReader<int> Generate(int nums,CancellationToken token=default)
{
    var channel=Channel.CreateBounded<int>(10);
    var writer=channel.Writer;

    //Create the actual "publisher" worker
    _ = Task.Run(async ()=>{
        for(int i=0;i<nums;i++)
        {
            //Check for cancellation
            if(token.IsCancellationRequested)
            {
                return;
            }

            await writer.WriteAsync(i*7);  
            await Task.Delay(100);        
        }
    }  ,token)
    //Complete and propagate any exceptions
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    //This casts to a ChannelReader
    return channel;
}
ChannelReader<double> Double(this ChannelReader<int> input,CancellationToken token=default)
{
    var channel=Channel.CreateBounded<double>(10);
    var writer=channel.Writer;

    //Create the actual "publisher" worker
    _ = Task.Run(async ()=>{
        await foreach(var msg in input.ReadAllAsync(token))
        {
            await writer.WriteAsync(2.0*msg);          
        }
    }  ,token)
    //Complete and propagate any exceptions
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    return channel;
}

ChannelReader<double> Root(this ChannelReader<double> input,CancellationToken token=default)
{
    var channel=Channel.CreateBounded<double>(10);
    var writer=channel.Writer;

    //Create the actual "publisher" worker
    _ = Task.Run(async ()=>{
        await foreach(var msg in input.ReadAllAsync(token))
        {
            await writer.WriteAsync(Math.Sqrt(msg));          
        }
    }  ,token)
    //Complete and propagate any exceptions
    .ContinueWith(t=>writer.TryComplete(t.Exception));

    return channel;
}
async Task Print(this ChannelReader<double> input,CancellationToken token=default)
{
    await foreach(var msg in input.ReadAllAsync(token))
    {
        Console.WriteLine(msg);
    }
}


await Generate(100)
          .Double()
          .Square()
          .Print();
using var cts=new CancellationTokenSource();
await Generate(100,cts.Token)
          .Double(cts.Token)
          .Square(cts.Token)
          .Print(cts.Token);
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

public static class Program
{
    public static async Task Main()
    {
        var channel = Channel.CreateUnbounded<int>();
        var bufferBlock = new BufferBlock<int>();
        var asyncCollection = new Nito.AsyncEx.AsyncCollection<int>();
        var mem0 = GC.GetTotalMemory(true);
        int timeouts = 0;
        for (int i = 0; i < 10; i++)
        {
            var stopwatch = Stopwatch.StartNew();
            while (stopwatch.ElapsedMilliseconds < 500)
            {
                using var cts = new CancellationTokenSource(1);
                try
                {
                    await channel.Reader.ReadAsync(cts.Token);
                    //await bufferBlock.ReceiveAsync(cts.Token);
                    //await asyncCollection.TakeAsync(cts.Token);
                }
                catch (OperationCanceledException) { timeouts++; }
            }
            var mem1 = GC.GetTotalMemory(true);
            Console.WriteLine($"{i + 1,2}) Timeouts: {timeouts,5:#,0},"
                + $" Allocated: {mem1 - mem0:#,0} bytes");
        }
    }
}
ChannelReader<Message> reader=_channel;

IAsyncEnumerable<IList<Message>> batchItems = reader.ReadAllAsync(token)
                                              .ToObservable()
                                              .Buffer(TimeSpan.FromSeconds(30), 5)
                                              .ToAsyncEnumerable();

await foreach(var batch in batchItems.WithCancellation(token))
{
 ....
}
public IAsyncEnumerable<T[]> BufferAsync(
            TimeSpan timeSpan,
            int count,
            CancellationToken cancellationToken = default)
{
    return _channel.Reader.BufferAsync(timeSpan,count,cancellationToken);
}
    //Returns all items in a period
    public static IAsyncEnumerable<IList<T>> BufferAsync<T>(
        this ChannelReader<T> reader, 
        TimeSpan timeSpan, 
        int count,
        CancellationToken token = default)
    {
        return reader.ReadAllAsync(token)
            .ToObservable()
            .Buffer(timeSpan, count)
            .ToAsyncEnumerable();
    }
        
        
    //Return the latest item in a period
    public static IAsyncEnumerable<T> SampleAsync<T>(
        this ChannelReader<T> reader, 
        TimeSpan timeSpan,
        CancellationToken token = default)
    {
        return reader.ReadAllAsync(token)
            .ToObservable()
            .Sample(timeSpan)
            .ToAsyncEnumerable();
    }