我在多个BackgroundServices
**[. NET 7]**中运行了类似的代码。在运行几天后 (显然,循环之间的延迟是几分钟(到几小时)),成千上万的悬空SqlConnection
句柄 (可能所有使用过的句柄仍然被引用,即使正确地与DB断开连接) 导致大量内存泄漏。
MRE
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);
}
}
}
1条答案
按热度按时间pkmbmrz71#
我相信这是github中的related issue。据我所知-这是SqlClient 5.0.1中引入的一个回归错误。基本上,在这一行:
传递标记,reader将在取消此标记时在内部注册回调函数。但是,此注册未在所有代码路径上正确注销。这反过来会导致您的
SqlConnection
示例可通过该回调注册从CancellationTokenSource
访问(该回调注册引用数据读取器、引用命令、引用连接)。这个问题在一个预览版本中得到了修复,所以你可以尝试安装5.1.0-preview 2,看看这个问题是否消失了。目前还没有包含这个修复的“生产”版本。