问题 使用CancellationToken取消SQL Server查询


我在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模式,我希望async方法 SqlCommand 拿走 CancellationToken 参数同样有效。不幸的是,我发现了这个问题 Cancel() 在...上 CancellationToken 引起了 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;

12771
2017-07-14 14:05


起源

你没有在取消任务时获得TaskCancellation Exception吗? - loop
@loop不,我在异步版本中没有得到TaskCancellationException。在非异步版本中,我在取消命令时得到了预期的SqlException。 - Dan
你可以尝试先取消任务后运行你的TestSqlServerCancelSprocExecution()。 - loop
@Dan - 这可能是因为当前的背景 - 我猜这里。请尝试cmd.ExecuteNonQueryAsync(cts.Token).ConfigureAwait(false); - Venki
@JohnSaunders我没有在任何地方读过这篇文章。我假设(显然不正确)因为TPL中的CancellationTokens显然是合作的,当它通过CancellationToken接收取消请求时,SqlCommand将在异步方法中使用相同的取消机制,就像它通过接收取消请求取消时一样。在同步方法中取消方法。 - Dan


答案:


在查看存储过程正在执行的操作后,它似乎以某种方式阻止了取消。

如果你改变了

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()。我不确定为什么会这样,但在同步方法中包装同步方法提供了类似的用法。


10
2017-07-18 21:25



这很好用。令人讨厌的是,WITH NOWAIT子句的存在是罪魁祸首,但至少有一个非常简单的解决方法。 - Dan
@CoderDennis,我正在使用这个解决方案,但是当我取消时抛出异常:“SqlException,...操作被用户取消”。你知道为什么吗 ? - Hamza_L
@Hamza_L自从我查看这段代码以来已经有一段时间了,但我认为这就是为什么我把任务包装在 try...catch 块。如果取消,则会引发异常。 - CoderDennis
@Hamza_L“用户取消操作”异常正是由于用户单击取消按钮触发取消操作所应该发生的情况。如果您不希望它引发异常,那么只需将其吞下即可。 - Kidquick
我能提出的最好的“吞下”代码是:catch(SqlException sqlex){switch(sqlex.ErrorCode){case -2146232060:/ * -2146232060相当通用,同时检查.Message * / if(!sqlex.Message) .Contains(“cancelled”)){throw;打破默认值:throw; }} - granadaCoder