最近我开始使用C#的MySQL驱动程序 https://github.com/mysql/mysql-connector-net。
在使用async/await时,我尝试在并行任务中运行简单的选择查询。
基本上,代码看起来像这样:
我期望的是两个查询并行运行,但我看到的是RunQueryA()开始运行,只有在它完成后才能运行RunQueryB。自然而然地,这表明查询中使用的一个或多个方法是阻塞的。为了找出原因,我从MySQL的GitHub存储库下载了最新的驱动程序源代码,并查找了异步方法的实现。例如,我查看了ExecuteReaderAsync的实现,它引导我到基类System.Data.Common.DbCommand,这是BCL的一部分。 我在.NET Reference源代码中查找了那个类 https://referencesource.microsoft.com/#System.Data/System/Data/Common/DBCommand.cs,1875e74763fd9ef2 但是我看到的内容真的让我很困惑:
一切归结为这行代码:
在这行代码中,ExecuteReader会同步运行并阻塞调用线程。
ExecuteReader调用一个抽象方法。
在使用async/await时,我尝试在并行任务中运行简单的选择查询。
基本上,代码看起来像这样:
private async Task<List<string>> RunQueryA()
{
List<string> lst = new List<string>();
using (MySqlConnection conn = new MySqlConnection(someConnectionString))
using (MySqlCommand cmd = conn.CreateCommand())
{
await conn.OpenAsync();
cmd.CommandText = "select someField from someTable ...";
using (var reader = await cmd.ExecuteReaderAsync())
{
// ...
}
}
return lst;
}
private async Task<List<string>> RunQueryB()
{
List<string> lst = new List<string>();
using (MySqlConnection conn = new MySqlConnection(someConnectionString))
using (MySqlCommand cmd = conn.CreateCommand())
{
await conn.OpenAsync();
cmd.CommandText = "select someField2 from someTable2 ...";
using (var reader = await cmd.ExecuteReaderAsync())
{
// ...
}
}
return lst;
}
public async Task Run()
{
await Task.WhenAll(RunQueryA(), RunQueryB());
}
我期望的是两个查询并行运行,但我看到的是RunQueryA()开始运行,只有在它完成后才能运行RunQueryB。自然而然地,这表明查询中使用的一个或多个方法是阻塞的。为了找出原因,我从MySQL的GitHub存储库下载了最新的驱动程序源代码,并查找了异步方法的实现。例如,我查看了ExecuteReaderAsync的实现,它引导我到基类System.Data.Common.DbCommand,这是BCL的一部分。 我在.NET Reference源代码中查找了那个类 https://referencesource.microsoft.com/#System.Data/System/Data/Common/DBCommand.cs,1875e74763fd9ef2 但是我看到的内容真的让我很困惑:
public Task<DbDataReader> ExecuteReaderAsync() {
return ExecuteReaderAsync(CommandBehavior.Default, CancellationToken.None);
}
public Task<DbDataReader> ExecuteReaderAsync(CancellationToken cancellationToken) {
return ExecuteReaderAsync(CommandBehavior.Default, cancellationToken);
}
public Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior) {
return ExecuteReaderAsync(behavior, CancellationToken.None);
}
public Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) {
return ExecuteDbDataReaderAsync(behavior, cancellationToken);
}
protected virtual Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) {
if (cancellationToken.IsCancellationRequested) {
return ADP.CreatedTaskWithCancellation<DbDataReader>();
}
else {
CancellationTokenRegistration registration = new CancellationTokenRegistration();
if (cancellationToken.CanBeCanceled) {
registration = cancellationToken.Register(CancelIgnoreFailure);
}
try {
return Task.FromResult<DbDataReader>(ExecuteReader(behavior));
}
catch (Exception e) {
registration.Dispose();
return ADP.CreatedTaskWithException<DbDataReader>(e);
}
}
}
一切归结为这行代码:
return Task.FromResult<DbDataReader>(ExecuteReader(behavior));
在这行代码中,ExecuteReader会同步运行并阻塞调用线程。
ExecuteReader调用一个抽象方法。
abstract protected DbDataReader ExecuteDbDataReader(CommandBehavior behavior);
这在MySQL驱动程序中被覆盖:
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
return ExecuteReader(behavior);
}
MySQL内部的实现基本上调用了ExecuteReader的同步版本...
简而言之,ExecuteReaderAsync()会同步运行ExecuteReader()并阻塞调用线程。
如果我有误,请纠正我,但它确实是这种情况。
我无法确定谁应该为此负责,是BCL的DbCommand类还是MySQL驱动程序的实现...
一方面,MySQL驱动程序应该考虑到这一点, 另一方面,由于DbCommand提供了ExecuteDbDataReaderAsync的基本实现,它应该至少在工作线程中启动ExecuteReader的同步版本(更不用说使用实际的异步I/O)以避免阻塞。
您对此有何看法?
有什么解决方法吗? 我可以自己启动ExecuteReaderAsync作为任务,但我不喜欢这个解决方案。
您有什么建议?
谢谢, Arik