【发布时间】:2017-04-01 11:03:47
【问题描述】:
编辑:这归结为为什么在异步代码中仅将 SqlConnection.Open() 更改为 await SqlConnection.OpenAsync() 会导致截然不同的行为。
除了明显的异步行为之外,同步代码中的 SqlConnection.Open 调用和异步代码中的 await SqlConnection.OpenAsync 调用有什么区别?底层连接是否与数据库异步?
关于 OpenAsync 的文档很精简,https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.openasync%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396。
Open 的异步版本,它打开一个数据库连接 使用 ConnectionString 指定的设置。这种方法 调用虚方法 OpenAsync CancellationToken.None.(继承自 DbConnection。)
我发现有趣的是,以前的连接字符串在其中需要 async=true,而在 .net 4.5+ 中不再需要它。连接的行为是否不同?
https://msdn.microsoft.com/en-us/library/hh211418(v=vs.110).aspx
从 .NET Framework 4.5 开始,这些方法不再需要 连接字符串中的异步处理=true。
当我碰巧在异步应用程序中使用同步 SqlConnection.Open 并大量加载它时,我发现它的性能很差,连接池很早就干涸了。我预计打开连接会阻塞,但是,在这些连接上执行异步命令(通过 dapper)的行为会有所不同。那么,OpenAsync 有何不同之处?
编辑:
按照要求的代码来重现问题(或者可能展示不同之处)。使用 Open() 运行此案例时,在执行大约 180 个并发异步命令时会遇到连接超时,而使用 OpenAsync() 即使在超过 300 个并发命令时也不会遇到异常。您可以推动并发最终使其超时,但它肯定会更深入地执行并发命令。
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using Nito.AsyncEx;
namespace AsyncSqlConnectionTest
{
class Program
{
public static int concurrent_counter = 0;
public static int total_counter = 0;
static void Main(string[] args)
{
var listToConsume = Enumerable.Range(1, 10000).ToList();
Parallel.ForEach(listToConsume,
new ParallelOptions { },
value =>
{
try
{
Task.Run(() => AsyncContext.Run(async () =>
{
using (var conn = new SqlConnection("Data Source=.; Database=master; Trusted_Connection=True;"))
{
Interlocked.Increment(ref concurrent_counter);
Interlocked.Increment(ref total_counter);
await conn.OpenAsync();
var result = await conn.QueryAsync("select * from master..spt_values; waitfor delay '00:00:05'");
Console.WriteLine($"#{total_counter}, concurrent: {concurrent_counter}");
Interlocked.Decrement(ref concurrent_counter);
}
})).GetAwaiter().GetResult();
}
catch (Exception e)
{
Console.Write(e.ToString());
}
});
Console.ReadLine();
}
}
}
编辑 2:
这是一个仅使用 ADO.NET 即可发现相同差异的测试。值得注意的是,Dapper 的执行速度要快得多,但这不是重点。同样,OpenAsync 最终会超时,但如果最大并行度为 100(低于连接池大小),则“稍后”并且永远不会。
using System;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsyncSqlConnectionTest
{
class Program
{
public static int concurrent_counter = 0;
public static int total_counter = 0;
static void Main(string[] args)
{
var listToConsume = Enumerable.Range(1, 10000).ToList();
Parallel.ForEach(listToConsume,
new ParallelOptions { },
value =>
{
try
{
Task.Run(async () =>
{
using (var conn = new SqlConnection("Data Source=.; Database=master; Trusted_Connection=True;"))
{
Interlocked.Increment(ref concurrent_counter);
Interlocked.Increment(ref total_counter);
// this (no errors)
await conn.OpenAsync();
// vs. this (timeouts)
//conn.Open();
var cmd = new SqlCommand("select * from master..spt_values; waitfor delay '00:00:05'", conn);
using (var reader = await cmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync()) { }
}
Console.WriteLine($"#{total_counter}, concurrent: {concurrent_counter}");
Interlocked.Decrement(ref concurrent_counter);
}
}).GetAwaiter().GetResult();
}
catch (Exception e)
{
Console.Write(e.ToString());
}
});
Console.ReadLine();
}
}
}
【问题讨论】:
-
内部从 .NET 4.5 开始,它始终是异步 IO。同步版本只是阻止...
-
running the connection pool dry early它的行为不应有所不同。线程阻塞在 IO 上还是在事件上几乎没有区别。executing asynchronous commands ... on those connections behaves differently可能是这种情况,我不知道。Asynchronous Processing=true在以前的 .NET 版本中一定有一些影响。我认为错误的问题是看 Open(Async) 而正确的问题是看Asynchronous Processing=true做了什么。但据此,它最多应该对 CPU 成本产生很小的影响:stackoverflow.com/a/7852617/122718 -
我不相信,但您应该在微基准测试中对其进行测试。启动一些工作负载,暂停调试器几次,然后查看 Parallel Stacks 窗口。观察库的内部调用堆栈以了解哪些代码对吞吐量有影响是非常有启发性的。从 .NET 4.5 开始,您应该会看到同步 API 的任务/事件上的所有线程都被阻塞,并且在异步情况下几乎没有线程处于活动状态。
-
您的应用程序中是否存在具体问题?如果不是,那很好,但如果是,那可能更容易回答。
-
这个案子现在让我很感兴趣。我看不出有什么不同的充分理由。一个想法:如果 Open 在内部阻塞并且线程池完全过载(这里显然是),那么阻塞可能需要很长时间,因为完成被阻塞的任务可能需要 TP 资源来完成 IO 完成处理。将 TP 设置为 5000 min/max threads 并将 MaxDOP for Parallel 设置为 500。差异应该会消失。
标签: c# asynchronous ado.net