无限循环中的BackgroundService Await Task.Delay 很难泄漏SqlConnection 对象

Cod*_*gry 7 .net c# sqlclient async-await .net-7.0

我有与此类似的代码在多个BackgroundServices (.NET 7)中运行。经过几天的运行时间(显然,循环之间的延迟为几分钟(到几小时))SqlConnection会导致数以万计的悬空句柄出现大量内存泄漏(可能所有曾经使用过的句柄仍然被引用,即使正确地与数据库)。我正在使用.NET 的 SqlClient 5.0.1。

磁力RE

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

using Microsoft.Data.SqlClient;

namespace Memleak;

static class Program
{
    const string connectionString = "Server=lpc:localhost;"
        + "Integrated Security=True;Encrypt=False;"
        + "MultipleActiveResultSets=False;Pooling=False;";

    static async Task Main(params string[] args)
    {
        using CancellationTokenSource cts = new();
        // not to forget it running
        cts.CancelAfter(TimeSpan.FromMinutes(15));
        CancellationToken ct = cts.Token;

        using Process process = Process.GetCurrentProcess();
        long loop = 1;

        while (true)
        {
            await ConnectionAsync(ct);

            // this seems to be the issue (delay duration is irrelevant)
            await Task.Delay(TimeSpan.FromMilliseconds(1), ct);

            process.Refresh();
            long workingSet = process.WorkingSet64;
            Console.WriteLine("PID:{0} RUN:{1:N0} RAM:{2:N0}",
                process.Id, loop, workingSet);

            ++loop;
        }
    }

    private static async Task ConnectionAsync(CancellationToken ct = default)
    {
        using SqlConnection connection = new(connectionString);
        await connection.OpenAsync(ct);

        using SqlCommand command = connection.CreateCommand();
        command.CommandText = "select cast(1 as bit);";

        using SqlDataReader reader = await command.ExecuteReaderAsync(ct);
        if (await reader.ReadAsync(ct))
        {
            _ = reader.GetBoolean(0);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

泄露

以下命令提示符命令显示泄漏:

// dotnet tool install --global dotnet-dump
dotnet-dump collect -p pid
dotnet-dump analyze dump_name.dmp
dumpheap -type Microsoft.Data.SqlClient.SqlConnection -stat
dumpheap -mt mtid
dumpobj objid
gcroot objid
Run Code Online (Sandbox Code Playgroud)

System.Threading.CancellationTokenSource+CallbackNode最后一个命令显示一个对象的巨大列表SqlConnection。

问题

这是一个错误还是按预期工作(如果是,为什么)?除了删除所有async代码并仅使用之外,是否有任何简单的解决方法Threads?我无法使用Timers,因为延迟会根据某些因素而变化(当有工作时,延迟会更短;当没有工作时,延迟会更长)。

非异步版本不会泄漏

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

using Microsoft.Data.SqlClient;

namespace NotMemleak;

static class Program
{
    const string connectionString = "Server=lpc:localhost;" +
        "Integrated Security=True;Encrypt=False;" +
        "MultipleActiveResultSets=False;Pooling=False;";

    static void Main(params string[] args)
    {
        using CancellationTokenSource cts = new();
        // not to forget it running
        cts.CancelAfter(TimeSpan.FromMinutes(15));
        CancellationToken ct = cts.Token;

        using Process process = Process.GetCurrentProcess();

        long loop = 1;
        while (loop < 1000)
        {
            Connection();

            // this seems to be the issue (delay duration is irrelevant)
            ct.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(1));
            // Thread.Sleep();

            process.Refresh();
            long workingSet = process.WorkingSet64;
            Console.WriteLine("PID:{0} RUN:{1:N0} RAM:{2:N0}"
                , process.Id, loop, workingSet);

            ++loop;
        }

        Console.WriteLine();
        Console.WriteLine("(press any key to exit)");
        Console.ReadKey(true);
    }

    private static void Connection()
    {
        using SqlConnection connection = new(connectionString);
        connection.Open();

        using SqlCommand command = connection.CreateCommand();
        command.CommandText = "select cast(1 as bit);";

        using SqlDataReader reader = command.ExecuteReader();
        if (reader.Read())
        {
            _ = reader.GetBoolean(0);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Evk*_*Evk 8

我相信这与 GitHub 中的这个问题有关。据我了解,这是 SqlClient 5.0.1 中引入的回归。基本上,在这条线上:

await reader.ReadAsync(ct)
Run Code Online (Sandbox Code Playgroud)

你传递一个token,当这个token被取消时,阅读器会在内部注册一个回调函数。但是,此注册并未在所有代码路径上正确取消注册。这反过来会导致您的SqlConnection实例可以CancellationTokenSource通过该回调注册(引用数据读取器、引用命令、引用连接)来访问。

此问题已在 5.1.0 中修复,该版本已于 2023-01-19 发布稳定版本。