BackgroundService等待任务.无限循环中的延迟会严重泄漏SqlConnection对象



我有类似的代码运行在多个BackgroundServices(。净7)。经过几天的运行时间(显然,循环之间的延迟是几分钟(到几小时))导致成千上万个悬挂的SqlConnection句柄(可能所有使用过的句柄仍然被引用,即使正确地从DB断开)中的大量内存泄漏。我正在使用。net的SqlClient 5.0.1。

绝笔

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);
}
}
}

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

// 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

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

这是一个bug还是按预期工作(如果是,为什么)?除了摆脱所有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);
}
}
}

我相信这与GitHub中的这个问题有关。据我所知,这是在SqlClient 5.0.1中引入的回归。基本上,在这一行:

await reader.ReadAsync(ct)

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

这个问题在5.1.0中修复了,它在2023-01-19有一个稳定的版本。

最新更新