C# MySql驱动程序-异步操作

5
最近我开始使用C#的MySQL驱动程序 https://github.com/mysql/mysql-connector-net
在使用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的一部分。

enter image description here

我在.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


这是MySQL的驱动程序/连接器。现在已经是2021年了,Oracle仍然不支持MySql C#的异步等待。 - jspinella
1个回答

11
DbCommand类至少从.NET 2.0就已经存在了。在.NET 4.5中,微软添加了ExecuteNonQueryAsyncExecuteReaderAsync等方法时,它们必须以向后兼容的方式进行。
最好的方法是按照.NET框架的做法:委托给现有的同步方法,并将其返回值包装在一个Task中。(通过在实现中调用Task.Run使方法“异步”几乎从来不是一个好主意;有关更详细的解释,请参见Should I expose asynchronous wrappers for synchronous methods?Task.Run Etiquette and Proper Usage。)
要获得真正的异步行为,数据库连接库的开发人员必须将其转换为真正的异步形式。这可能很困难;使大型同步代码库变为异步可能涉及重写大部分代码。
目前,Oracle的MySQL .NET连接器没有实现真正的异步方法。MySQL Bug 70111报告了MySQL连接器中的这个问题。该问题在此提问中也有进一步讨论。

我建议使用一个我一直在开发的库: NuGet上的MySqlConnectorGitHub。它是一个完全独立、完全异步实现MySQL协议的.NET和.NET Core的库。API与官方的MySql.Data连接器相同,因此对于大多数需要真正异步数据库连接的项目来说,它应该是一个无缝替换的选择。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接