使用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() ,我可以validation基础存储过程基本上立即停止执行。 鉴于我在我的应用程序中使用了异步/等待模式,我希望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; 

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

如果你改变了

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

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

您可以通过其他方式跟踪长时间运行的存储过程的进度或注册令牌取消并调用cmd.Cancel()因为您知道它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(); } 

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