使用CancellationToken取消SQL Server查询

时间:2014-07-14 14:05:53

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

我在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()后,我可以验证基础存储过程是否基本上立即停止执行。鉴于我在我的应用程序中使用了异步/等待模式,我希望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()的调用不会停止执行底层存储过程。 / p> 编辑:这是我重要的存储过程的主体。它以一秒的间隔插入记录并打印结果,以便于查看取消尝试是否立即生效。

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;

1 个答案:

答案 0 :(得分:12)

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

如果你改变了

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