在C#中使用异步方式与SQLite交互

6
我正在努力理解async/await关键字和用法,我认为我已经掌握了基础知识。但是我的SQLite代码出了些问题。
我在一个简单的项目中使用SQLite.core NuGet package。我注意到我编写的异步代码并没有像我预期的那样异步执行,因此我创建了一个更简单的测试项目来测试我的理解。
在我的测试代码中,我打开一个连接到内存数据库(我在基于文件的数据库中遇到了同样的问题。在测试代码中,内存更容易),然后发出一个单独的“create table”命令,使用ExecuteNonQueryAsync。我不会立即await结果,而是先将一些内容写入控制台,最后再使用await关键字。
我希望在ExecuteNonQueryAsync完成之前执行控制台命令,因此在我的测试中,我应该看到“1 2 3 4”。但是我得到的却是“1 3 2 4”。
我使用 SQL Server LocalDB 连接运行了一个相同的测试(运行相同的代码,只是 DbConnection 不同),得到了预期的 "1 2 3 4"。所以我猜我的 async 的基本理解并没有太远离正确的方向。
我错过了什么?我需要使用特殊的连接字符串来支持 SQLite 的 async 方法吗?它是否支持?
我的完整测试项目可以在 这里 找到。
以下是主程序:
 namespace DatabaseTest
   {
    using System;
    using System.Data.Common;
    using System.Data.SqlClient;
    using System.Data.SQLite;
    using System.Threading.Tasks;
class Program
{
    static void Main(string[] args)
    {
        Task.WaitAll(TestDatabase(true), TestDatabase(false));
    }

    private static async Task TestDatabase(bool sqLite)
    {
        Console.WriteLine("Testing database, sqLite: {0}", sqLite);
        using (var connection = CreateConnection(sqLite))
        {
            connection.Open();
            var task = ExecuteNonQueryAsync(connection);
            Console.WriteLine("2");
            await task;
            Console.WriteLine("4");
        }
    }

    private static DbConnection CreateConnection(bool sqLite)
    {
        return sqLite ?
            (DbConnection)new SQLiteConnection(string.Format("Data Source=:memory:;")) :
            new SqlConnection(@"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\DatabaseTest.mdf;Integrated Security=True;Connect Timeout=30");
    }

    private static async Task ExecuteNonQueryAsync(DbConnection connection)
    {
        var command = connection.CreateCommand();
        command.CommandText = "CREATE TABLE test (col1 integer);";
        Console.WriteLine("1");
        await command.ExecuteNonQueryAsync();
        Console.WriteLine("3");
    }
}

输出结果:

Testing database, sqLite: True
1
3
2
4
Testing database, sqLite: False
1
2
3
4
3个回答

15

System.Data.SQLite实现是100%同步的。它们没有任何异步重载,这是因为Microsoft导致了这种误解,因为SQLiteCommand通过默认实现*Async方法扩展System.Data.Common.DbCommand,这些方法只调用同步版本:

/// <summary>This is the asynchronous version of <see cref="M:System.Data.Common.DbCommand.ExecuteNonQuery" />. Providers should override with an appropriate implementation. The cancellation token may optionally be ignored.The default implementation invokes the synchronous <see cref="M:System.Data.Common.DbCommand.ExecuteNonQuery" /> method and returns a completed task, blocking the calling thread. The default implementation will return a cancelled task if passed an already cancelled cancellation token.  Exceptions thrown by <see cref="M:System.Data.Common.DbCommand.ExecuteNonQuery" /> will be communicated via the returned Task Exception property.Do not invoke other methods and properties of the <see langword="DbCommand" /> object until the returned Task is complete.</summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>A task representing the asynchronous operation.</returns>
/// <exception cref="T:System.Data.Common.DbException">An error occurred while executing the command text.</exception>
public virtual Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken)
{
  ...
  return Task.FromResult<int>(this.ExecuteNonQuery());
  ...
}

我也是通过相同的艰难方式才明白,而且我对他们采取的方法并不满意,但这就是我们所得到的。仅供记录,我认为应该使用NotSupportedException


谢谢,我一直在想为什么ExecuteReaderAsync返回一个DbDataReader - hoss
3
Microsoft.Data.Sqlite 实现一样,异步 ADO.NET 方法会同步执行。https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/async - apdevelop
1
几乎所有的ADO.NET数据库提供程序实现都是同步的,除了针对MS SQL Server的那个是完全异步的!System.Data.SQLite也是同步的,因为你可以使用预写式日志(https://www.sqlite.org/wal.html),这里也建议使用。始终使用事务块来最小化线程阻塞! - Martin.Martinsson

3

一旦您启动了异步任务,该任务和主线程都可以继续运行。因此,无法保证哪个会更快。

SQLite是一个嵌入式数据库,没有客户端/服务器通信开销,并且作为库在相同的CPU上运行。因此,这个实现可能已经决定支持异步执行没有意义。


请问您能否提供更多关于此事的信息?我真的不知道为什么会发生这种情况。 - FatemehEbrahimiNik
作为一个库,它在同一CPU上运行。它如何保证在完全相同的CPU上运行?托管库没有这样的控制。 - Yuval Itzchakov
@YuvalItzchakov 没有保证。但从操作系统的角度来看,调用库函数并不会改变此进程的调度。(在 SQL Server 中,数据库代码始终异步运行在命名管道的另一端。) - CL.
所以基本上,我“不用担心”,可以使用await编写我的代码。如果SQLite决定需要使用不同的线程,我将获得好处。如果它运行“足够快”,我就不必在意了。 - Noam Gal

2

几乎所有的ADO.NET数据库提供程序实现都是同步的(异步 -> 内部路由到同步实现),除了针对MS SQL Server的一个完全异步的实现!

System.Data.SQLite .NET数据提供程序也是同步的,因为您可以使用预写日志https://www.sqlite.org/wal.html),这里也可能建议使用。始终使用事务块来最小化线程阻塞(大大加快速度)!

AFAIK同步.NET数据提供程序:

  • 所有SQLite提供程序
  • Sybase(SQL Anywhere)
  • PostgreSQL
  • Oracle MySQL数据提供程序

异步的有:

  • MS SQL Server数据提供程序位于System.Data中
  • MySQL的MySqlConnector
  • DevArt商业提供程序,但它们没有TAP(任务异步模式)

也许编写自己的扩展并将该方法放入Task.Run(() => Func())。

提示:当您想检查您的“.NET数据提供程序代码”是否在与主进程线程不同的线程中运行时,请在Visual Studio或VS Code的调试菜单中显示“线程窗口”。
敬礼

1
"也许编写自己的扩展并将方法放入Task.Run中" -- 不要这样做! - Theodor Zoulias
@TheodorZoulias,根据那篇文章所说 - "如果开发人员需要通过同步API实现响应性或并行性,他们可以简单地使用Task.Run之类的方法来包装调用。"如果这是OP的代码,那我同意你的观点 - 但是对于同步的Sqlite API来说,如果你想要应用程序的响应性,你没有太多选择! - undefined
@Moo-Juice 我并不反对使用 Task.Run。我反对的是暴露一个假装是真正异步的 API。Task.Run 应该在应用程序代码中使用,而不是在库代码中使用。不要隐藏它,让它可见。例如,在你的 async void button_Click 事件处理程序中直接使用它,而不是在被处理程序调用的异步方法中使用。这就是微软的文章所推荐的。 - undefined
1
@TheodorZoulias,啊哈,这是一个重要的区别,我完全同意你的观点! - undefined

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