当调用SQL Server实际执行需要花时间的事情时,SqlDataReader.ReadAsync()
会同步运行。有没有办法强制它以异步方式运行,或者是我在Task.Run()
中调用它的唯一选择?
这是一个复制品。它使用winforms来演示调用阻止GUI线程。请注意,T-SQL必须实际执行某些操作 - 使用WAITFOR DELAY '00:00:20'
无法重现。
using System;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Windows.Forms;
static class SqlDataReaderReadAsyncProgram
{
static async void Form_Shown(object sender, EventArgs e)
{
var form = (Form)sender;
// Declare your connection string in app.config like
// <connectionStrings><remove name="LocalSqlServer"/><add name="LocalSqlServer" connectionString="Data Source=localhost\SQLEXPRESS;Integrated Security=true"/></connectionStrings>
using (DbConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString))
{
form.Text = "connecting…";
await connection.OpenAsync();
form.Text = "connected!";
// Install a stored procedure.
using (var command = connection.CreateCommand())
{
command.CommandText = "SET NOCOUNT ON"
+ " SELECT 'a'"
+ " DECLARE @t DATETIME = SYSDATETIME()"
+ " WHILE DATEDIFF(s, @t, SYSDATETIME()) < 20 BEGIN"
+ " SELECT 2 x INTO #y"
+ " DROP TABLE #y"
+ " END"
+ " SELECT 'b'";
form.Text = "executing…";
using (var reader = await command.ExecuteReaderAsync())
{
form.Text = "reading…";
do
{
// Blocks on the second call until the second resultset is returned by SQL Server
while (await reader.ReadAsync())
{
}
} while (await reader.NextResultAsync());
form.Text = "done!";
}
}
}
await Task.Delay(TimeSpan.FromSeconds(5));
form.Close();
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var form = new Form();
form.Shown += Form_Shown;
Application.Run(form);
}
}
当我运行它时,窗口在报告完成之前变为“(无响应)”20秒(请注意,在VS中调试时,“(无响应)”文本不会出现,但它仍会冻结相同的内容) 。如果我在VS中进行调试并在冻结时将其分解,我会看到它与一个看起来像这样的调用堆栈:
[Managed to Native Transition]
System.Data.dll!SNINativeMethodWrapper.SNIReadSyncOverAsync(System.Runtime.InteropServices.SafeHandle pConn, ref System.IntPtr packet, int timeout) Unknown
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.ReadSniSyncOverAsync() Unknown
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryReadNetworkPacket() Unknown
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryPrepareBuffer() Unknown
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryReadByteArray(byte[] buff, int offset, int len, out int totalRead) Unknown
System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryReadInt64(out long value) Unknown
System.Data.dll!System.Data.SqlClient.TdsParser.TryProcessDone(System.Data.SqlClient.SqlCommand cmd, System.Data.SqlClient.SqlDataReader reader, ref System.Data.SqlClient.RunBehavior run, System.Data.SqlClient.TdsParserStateObject stateObj) Unknown
System.Data.dll!System.Data.SqlClient.TdsParser.TryRun(System.Data.SqlClient.RunBehavior runBehavior, System.Data.SqlClient.SqlCommand cmdHandler, System.Data.SqlClient.SqlDataReader dataStream, System.Data.SqlClient.BulkCopySimpleResultSet bulkCopyHandler, System.Data.SqlClient.TdsParserStateObject stateObj, out bool dataReady) Unknown
System.Data.dll!System.Data.SqlClient.SqlDataReader.TryHasMoreRows(out bool moreRows) Unknown
System.Data.dll!System.Data.SqlClient.SqlDataReader.TryReadInternal(bool setTimeout, out bool more) Unknown
System.Data.dll!System.Data.SqlClient.SqlDataReader.ReadAsync.AnonymousMethod__0(System.Threading.Tasks.Task t) Unknown
System.Data.dll!System.Data.SqlClient.SqlDataReader.InvokeRetryable<bool>(System.Func<System.Threading.Tasks.Task, System.Threading.Tasks.Task<bool>> moreFunc, System.Threading.Tasks.TaskCompletionSource<bool> source, System.IDisposable objectToDispose) Unknown
System.Data.dll!System.Data.SqlClient.SqlDataReader.ReadAsync(System.Threading.CancellationToken cancellationToken) Unknown
System.Data.dll!System.Data.Common.DbDataReader.ReadAsync() Unknown
> SqlDataReaderReadAsync.exe!SqlDataReaderReadAsyncProgram.Form_Shown(object sender, System.EventArgs e) Line 36 C#
[Resuming Async Method]
(为了简洁而进一步修剪)。
整个ReadSyncOverAsync
内容对我来说特别可疑。这就像SqlClient假设同步读取不会阻塞,好像它不知道如何使用非阻塞IO或其他东西。然而,当查看参考源或使用JustDecompile进行反编译时,看起来应该是异步支持,但它只是在某种程度上启发式/后退决定不使用它。
那么,如何让SqlClient中的* Async()
内容真正异步?我认为这些方法应该能让我编写无线响应式GUI程序而无需使用Task.Run()
,因为在Task.Run()
中包装同步事件只是为了使它们异步是没有意义的开销......?
我正在使用.net-4.7.02542。
我假设这是一个.net错误并提交了connect #3139210(编辑:连接已经死了,我在https://github.com/binki/connect3139210有一个repro项目。)
更新:Microsoft承认该错误并将其修复为.net-4.7.3。 我使用VS订阅中的“技术支持”案例来报告错误并获取此信息。
答案 0 :(得分:0)
Microsoft在.net-4.8中发布了针对此问题的修复程序。我已经测试并验证了它的有效性。我还没有看到.net-4.7.3的版本,所以我不知道它是否真正包含该修复程序。
regedit中与releaseKey = 528040相关的SKU:
您的应用程序必须以.net-4.8为目标才能获得修复(仅安装更新不能修复已编译的应用程序)。不幸的是,此功能没有文档<AppContextSwitchOverrides/>
,因此,如果必须继续定位较早版本的.net,则不能选择使用此修补程序。 (但是,您可以在编译时将.net-4.8定位为目标,编辑«ProgramName».config
来更改<supportedRuntime/>
,然后注意不要在目标版本之后使用.net中引入的任何API。 )。