【问题标题】:Canceling SQL Server query with CancellationToken使用 CancellationToken 取消 SQL Server 查询
【发布时间】:2014-07-14 14:05:53
【问题描述】:

我在 SQL Server 中有一个长时间运行的存储过程,我的用户需要能够取消它。我编写了一个小型测试应用程序,如下所示,证明SqlCommand.Cancel() 方法工作得很好:

    private SqlCommand cmd;
    private void TestSqlServerCancelSprocExecution()
    {
        TaskFactory f = new TaskFactory();
        f.StartNew(() =>
            {
              using (SqlConnection conn = new SqlConnection("connStr"))
              {
                conn.InfoMessage += conn_InfoMessage;
                conn.FireInfoMessageEventOnUserErrors = true;
                conn.Open();

                cmd = conn.CreateCommand();
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "dbo.[CancelSprocTest]";
                cmd.ExecuteNonQuery();
              }
           });
    }

    private void cancelButton_Click(object sender, EventArgs e)
    {
        if (cmd != null)
        {
            cmd.Cancel();
        }
    }

调用cmd.Cancel() 后,我可以验证底层存储过程是否立即停止执行。鉴于我在我的应用程序中大量使用了 async/await 模式,我希望 SqlCommand 上采用 CancellationToken 参数的异步方法同样可以正常工作。不幸的是,我发现在CancellationToken 上调用Cancel() 导致InfoMessage 事件处理程序不再被调用,但底层存储过程继续执行。我的异步版本测试代码如下:

    private SqlCommand cmd;
    private CancellationTokenSource cts;
    private async void TestSqlServerCancelSprocExecution()
    {
        cts = new CancellationTokenSource();
        using (SqlConnection conn = new SqlConnection("connStr"))
        {
            conn.InfoMessage += conn_InfoMessage;
            conn.FireInfoMessageEventOnUserErrors = true;
            conn.Open();

            cmd = conn.CreateCommand();
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.CommandText = "dbo.[CancelSprocTest]";
            await cmd.ExecuteNonQueryAsync(cts.Token);
        }
    }

    private void cancelButton_Click(object sender, EventArgs e)
    {
        cts.Cancel();
    }

我是否遗漏了 CancellationToken 的工作方式?我使用的是 .NET 4.5.1 和 SQL Server 2012,以防万一。

编辑:我将测试应用程序重写为控制台应用程序,以防同步上下文是一个因素,并且我看到相同的行为——CancellationTokenSource.Cancel() 的调用不会停止底层存储过程的执行。

编辑:这是我调用的存储过程的主体,以防万一。它以一秒的间隔插入记录并打印结果,以便轻松查看取消尝试是否及时生效。

WHILE (@loop <= 40)
BEGIN

  DECLARE @msg AS VARCHAR(80) = 'Iteration ' + CONVERT(VARCHAR(15), @loop);
  RAISERROR (@msg,0,1) WITH NOWAIT;
  INSERT INTO foo VALUES (@loop);
  WAITFOR DELAY '00:00:01.01';

  SET @loop = @loop+1;
END;

【问题讨论】:

  • 您在取消任务时没有收到 TaskCancellation 异常吗?
  • 可以先取消任务后尝试运行TestSqlServerCancelSprocExecution()吗?
  • @Dan - 这可能是因为当前的上下文 - 我在这里猜测。请尝试 cmd.ExecuteNonQueryAsync(cts.Token).ConfigureAwait(false);
  • @JohnSaunders 我没有在任何地方读到过。我假设(显然是错误的)因为 TPL 中的 CancellationTokens 是明确合作的,所以当 SqlCommand 通过 CancellationToken 收到取消请求时,它会在异步方法中使用相同的取消机制,就像它通过同步方法中的取消方法。
  • 对于其他遇到此问题的人,在异步取消中存在一个已知错误,请参阅github.com/dotnet/SqlClient/issues/44

标签: c# sql-server async-await cancellationtokensource


【解决方案1】:

查看您的存储过程在做什么之后,它似乎以某种方式阻止了取消。

如果你改变了

RAISERROR (@msg,0,1) WITH NOWAIT;

删除WITH NOWAIT 子句,然后取消按预期工作。但是,这会阻止InfoMessage 事件实时触发。

您可以通过其他方式跟踪长期运行的存储过程的进度,或者注册令牌取消并调用cmd.Cancel(),因为您知道这可行。

另外需要注意的是,在 .NET 4.5 中,您可以只使用 Task.Run 而不是实例化 TaskFactory

所以这是一个可行的解决方案:

private CancellationTokenSource cts;
private async void TestSqlServerCancelSprocExecution()
{
    cts = new CancellationTokenSource();
    try
    {
        await Task.Run(() =>
        {
            using (SqlConnection conn = new SqlConnection("connStr"))
            {
                conn.InfoMessage += conn_InfoMessage;
                conn.FireInfoMessageEventOnUserErrors = true;
                conn.Open();

                var cmd = conn.CreateCommand();
                cts.Token.Register(() => cmd.Cancel());
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.CommandText = "dbo.[CancelSprocTest]";
                cmd.ExecuteNonQuery();
            }
       });
    }
    catch (SqlException)
    {
        // sproc was cancelled
    }
}

private void cancelButton_Click(object sender, EventArgs e)
{
    cts.Cancel();
}

在对此进行测试时,我必须将 ExecuteNonQuery 包裹在 Task 中,以便 cmd.Cancel() 工作。如果我使用ExecuteNonQueryAsync,即使没有传递令牌,系统也会阻塞cmd.Cancel()。我不确定为什么会这样,但是将同步方法包装在 Task 中提供了类似的用法。

【讨论】:

  • 这很好用。令人讨厌的是 WITH NOWAIT 子句的存在是罪魁祸首,但至少有一个非常简单的解决方法。
  • @CoderDennis,我正在使用这个解决方案,但是当我取消时抛出异常:“SqlException,......用户取消的操作”。你知道为什么吗?
  • @Hamza_L 自从我查看这段代码以来已经有一段时间了,但我认为这就是我将任务运行包装在 try...catch 块中的原因。如果您取消,它将引发异常。
  • @Hamza_L “操作被用户取消”异常正是应该发生的,因为取消操作是由用户单击取消按钮触发的。如果您不希望它引发异常,那么只需吞下它。
  • 我能想到的最好的“吞噬”代码是:catch (SqlException sqlex) { switch (sqlex.ErrorCode) { case -2146232060: /* -2146232060 相当通用,请检查 .Message */ if (!sqlex.Message.Contains("cancelled")) { throw; } 休息;默认值:抛出; } }
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-01
  • 2018-06-06
  • 1970-01-01
相关资源
最近更新 更多