如何正确地在SqlConnection和SqlDataReader上使用异步方法?

3

我正在将一些旧的ASP.NET代码移植到.NET 4.5上。我习惯于以老式方式使用SqlConnection/SqlCommand/SqlDataReader。现在,我正在尝试使用异步原语来提高性能。

这是我打开连接的旧代码:

    static DBWrapper Open(string connKey) {
        string connStr = WebConfigurationManager.ConnectionStrings[connKey].ConnectionString;
        SqlConnection c = new SqlConnection(connStr);
        c.Open();
        DBWrapper retVal = new DBWrapper();
        retVal._c = c;
        return retVal;
    }

DBWrapper类只是SqlConnection的一个轻量级包装器,具有我需要的一些额外方法。

下面是我如何转换为新的异步样式:

    static async Task<DBWrapper> Open(string connKey) {
        string connStr = WebConfigurationManager.ConnectionStrings[connKey].ConnectionString;
        SqlConnection c = new SqlConnection(connStr);
        var v = c.OpenAsync();
        DBWrapper retVal = new DBWrapper();
        await v;
        retVal._c = c;
        return retVal;
    }

基本上,在连接正在打开时,我可以同时创建一个新的DBWrapper对象。

我困惑的是这个函数到底表现得好还是不好。任务创建的开销和等待它的时间可能比创建一个新的DBWrapper对象所需的时间要多得多。无论是否使用async,我最终都会返回一个完全构建好的连接对象。

这是一个从数据库返回用户列表的方法:

 List<MyUsers> getUsers() {
     using(SqlCommand cmd ...) {
       using(SqlDataReader reader ...) {
          while(reader.Read() {
             ... Fill the list
          }
       }
 }

我也可以将这个方法转换为异步风格:

我也可以将这个方法转换为异步风格:

while (await reader.ReadAsync())
  ... Fill the list
}

再一次,我没有看到任何性能提升。事实上,在第二个示例中,即使在等待ReadAsync()期间,我也不会同时执行任何其他操作。

这是正确的方法还是有更好的使用异步范例与数据库连接和查询的方法?

1个回答

20
请记住,async不意味着“更快”,它意味着“并发”。也就是说,async的目的是在操作进行时可以同时做其他事情。在UI应用程序中,“其他事情”通常是“响应用户输入”; async对于UI应用程序最主要的好处是响应能力。对于ASP.NET应用程序,它的工作方式略有不同... 如果您在整个过程中都使用了async,那么它在ASP.NET应用程序上的主要好处是可伸缩性。所谓“整个过程”,是指如果您的List<MyUsers> getUsers()方法变成了Task<List<MyUsers>> getUsersAsync(),并且调用它的方法变为async,以此类推一直到您的MVC action / WebPage方法 / 等等(这些方法也是async),那么您就是“全程async”。
在这种情况下,您会获得可伸缩性的好处。请注意,每个单独的操作不会运行得更快,但是您的应用程序整体将具有更好的可伸缩性。当await让出控制权时,ASP.NET运行时会释放请求线程,以便在数据库连接接收下一行数据时处理其他请求。对于同步代码,每当您的应用程序在与数据库通信时等待I/O时,它都会阻塞一个线程(什么也不做)。async在等待I/O时释放该线程。
因此,整个应用程序可以具有更好的可伸缩性,因为它更好地利用了线程池线程。

有时候利用请求内部的并发可以获得一些收益。例如,如果一个请求需要访问数据库和调用一些Web API,那么您可以同时启动它们并使用 await Task.WhenAll(...) 异步等待两个结果。在您的第一个代码示例中,您正在同时进行db连接和对象创建,但我怀疑除非您的构造函数确实非常耗费计算资源,否则在这种情况下您不会真正看到任何好处。如果您有两个I/O操作或一个真正的CPU操作,则async可以帮助您在请求内部执行并发操作,但主要用例是提高整个应用程序的可扩展性。


感谢您提供详细的答案。只是为了确认,您认为在上述两种特定情况下使用异步方法的开销是合理的。这正确吗? - Peter
2
异步的开销几乎可以忽略不计。如果您有一个可扩展的后端(例如 Azure SQL),那么我会在任何 I/O 上使用异步。唯一不使用异步的地方是:1)后端不可扩展(例如单个 SQL Server 实例)并且2)大多数请求都命中后端并且3)您已经有了同步解决方案。在这种情况下,转换为异步不会带来任何好处。但是,如果您的后端可以扩展,或者有很多请求执行其他操作(而不是命中后端),或者您正在启动一个新的解决方案,则请使用异步。 - Stephen Cleary
@StephenCleary 给出了非常周到的指导,但是如果每个异步任务都在访问 SQL Server 的单个实例,但所涉及表的文件组位于不同的磁盘上怎么办呢?例如:主数据库和历史数据库,或前台数据库和后台数据库。我希望扩展性仅受附加存储的网络适配器限制。现在云端一切都使用 SSD。 - John Zabroski
@JohnZabroski 当然可以,那么你可能需要考虑转换为异步。我总是将新代码编写为异步 - 这没有任何问题 - 但是否值得转换为异步是另一个问题。 - Stephen Cleary
1
@yv989c:一般情况下不是这样的。在只有一个数据库后端的特定情况下,当然,扩展您的Web服务器不会帮助整个系统的可扩展性。但在云场景中,例如Azure SQL后端,async非常有用。请参见异步代码不是Async ASP.NET中的万能药 - Stephen Cleary
显示剩余5条评论

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