【问题标题】:SQLException when Canceling Async Query取消异步查询时出现 SQLException
【发布时间】:2013-09-21 03:00:40
【问题描述】:

使用带有 CancellationToken 的新 .Net 4.5 Async/Await 功能,当我取消 ExecuteNonQueryAsync 调用时,我得到一个 SQLException,而不是 OperationCanceledException(或其他一些特定于取消操作的异常) )。 SQLException 确实在消息末尾说 Operation cancelled by user。我希望在取消操作时会引发更具体的异常。此外,我将如何创建适当的 Try/Catch 处理程序来处理这种预期情况?我通常会将SQLException 作为更一般的故障块,但现在我必须梳理消息的文本,看看这是否只是用户单击取消按钮!?我一定是错过了什么。

这是一个简单的 VB WinForm 应用程序,它有两个按钮,一个用于执行异步调用,另一个用于取消。第一个按钮中的 Try/Catch 显示了在第二个按钮调用 Cancel 方法时被点击的 SQLException

Dim _cts As CancellationTokenSource
Private Async Sub btnLocalTest_Click(sender As Object, e As EventArgs) Handles btnLocalTest.Click
    _cts = New CancellationTokenSource()
    Dim CancelToken As CancellationToken = _cts.Token
    Using sconn As New SqlConnection("server=(local);database=MyDB;user id=MyUser;password=MyPassword")
        sconn.Open()
        Dim SQL As String = some long running SELECT or INSERT statement
        Try
            Using scmd As New SqlCommand(SQL, sconn)
                scmd.CommandTimeout = 300
                Dim i As Integer = Await scmd.ExecuteNonQueryAsync(CancelToken)
            End Using
        Catch exCancel As OperationCanceledException
            LogEvent("Query canceled Exception.") ' This error is *not* thrown on Cancel.  
        Catch ex As SqlClient.SqlException
            LogEvent("Error with query. " & ex.Message)  ' This error *is* thrown on Cancel.  Message includes text 'Canceled by user.'
        End Try
        sconn.Close()
        _cts = Nothing
    End Using
End Sub

Private Sub btnLocalTestCancel_Click(sender As Object, e As EventArgs) Handles btnLocalTestCancel.Click
    If _cts IsNot Nothing Then
        _cts.Cancel()
    End If
End Sub

更新:我使用支持取消的HttpClient.GetAsync 方法创建了一个不同的异步测试。当您取消该任务时,您可以看到我在上面最初预期的OperationCanceledException 异常。 所以问题仍然存在:取消异步任务时应该得到什么异常?还是取决于每种方法及其实现?

【问题讨论】:

  • 使用 SqlException.Number。
  • SqlException.Number = 0。SqlException.Errors(1).Number = 0。不是很有帮助。
  • 我遇到了同样的问题。您找到了解决方案还是使用了 Message 属性?
  • 不,我没有发现取消 Async 方法时抛出一致的异常类。
  • 你为什么不使用 OpenAsync() ?

标签: .net async-await


【解决方案1】:

我通过检查 Catch ex As SqlClient.SqlException 块中的 CancelToken.IsCancellationRequested“解决了”这个问题。

【讨论】:

    【解决方案2】:

    这并不总是像捕获SqlException 那样简单。如果您在异步任务上使用Task.Wait(),那么SqlException 将被包裹在AggregateException 中。

    可以在 MSDN 上的 ADO.NET 部分的文章 Asynchronous Programming(“取消异步操作”部分)中找到演示此操作的代码示例。

    Task 类通用的类似行为的文档隐藏在 MSDN 文章 Task Cancellation(“任务并行库”文档的一部分)中,尽管这里 AggregateException 包装了 TaskCanceledException(派生自OperationCanceledException)。

    这是一段稍微简化的 C# 代码,显示了我当前如何使用 SqlClient 处理取消请求:

    class MyDataProcessor
    {
        void ReadSomething(CancellationToken cancellationToken)
        {
            try
            {
                // Get the command from somewhere
                DbCommand dbCommand = [...]
    
                // We don't use await, we manage the Task ourselves
                Task<DbDataReader> task = dbCommand.ExecuteReaderAsync(cancellationToken)
                // If cancellation is requested this throws an AggregateException
                task.Wait();
    
                // Task status should probably be checked here,
                // but this is just sample code
                DbDataReader dbDataReader = task.Result;
    
                // If cancellation is requested, this throws a straight SqlException
                while (dbDataReader.Read())
                {
                    // do something with the data
    
                    // Be nice and check the token. ADO.NET data providers
                    // other than SqlClient might not check the token.
                    cancellationToken.ThrowIfCancellationRequested();
                }
            }
            catch (System.Exception exception)
            {
                // If it's a cancellation request, transform the SqlException
                // into an OperationCanceledException
                ThrowIfSqlClientCancellationRequested(
                    cancellationToken, exception);
    
                // Re-throw if it's a genuine error
                throw;
            }
        }
    
        void ThrowIfSqlClientCancellationRequested(
            CancellationToken cancellationToken,
            Exception exception)
        {
            // Check the CancellationToken, as suggested by Anton S in his answer
            if (!cancellationToken.IsCancellationRequested)
                return;
            System.Data.SqlClient.SqlException sqlException =
                exception as System.Data.SqlClient.SqlException;
            if (null == sqlException)
            {
                AggregateException aggregateException = exception as AggregateException;
                if (null != aggregateException)
                    sqlException = aggregateException.InnerException as System.Data.SqlClient.SqlException;
                if (null == sqlException)
                    return;
            }
            // Assume that if it's a "real" problem (e.g. the query is malformed),
            // then this will be a number != 0, typically from the "sysmessages"
            // system table 
            if (sqlException.Number != 0)
                return;
            throw new OperationCanceledException();
        }
    }
    

    我对此不太满意,它看起来很脆弱,但缺少任何官方文档,这是我目前能想到的最好的。主要问题是:

    • SqlClient 的未来版本会改变其取消行为吗?
    • 上面的代码是否包含其他行为?

    【讨论】:

    • 嗯……调用Task.Wait() 意味着你阻塞了当前线程。如果查询运行的时间足够长以至于您认为值得使用 CancellationToken,我建议您也查看async/await。在这种情况下,我建议使用同步 API,但我想这是取消正在运行的查询的最简单方法,即使您不购买 async/await
    • @binki ReadSomething() 函数已经在单独的线程上运行。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多