【问题标题】:Cancelling a Thread due to hung Db call由于挂起 Db 调用而取消线程
【发布时间】:2014-03-26 17:43:36
【问题描述】:

我已经设计并制作了一个原型应用程序,用于作为 Windows 服务 (C#) 运行的高性能、多线程邮件合并。这个问题是指问题的一个棘手部分,如果进程挂在数据库调用上该怎么办。我对此进行了很多研究。我已经阅读了很多关于线程取消的文章,但我最终只看到了一种方法,thread.Abort()。是的,我知道,绝对不要使用 Thread.Abort(),所以几天来我一直在研究如何以另一种方式进行操作,并且在我看来,别无选择。我会告诉你为什么,希望你能告诉我为什么我错了。

仅供参考,这些都是长时间运行的线程,因此 TPL 无论如何都会将它们置于 ThreadPool 之外。

TPL 只是线程的一个很好的包装器,所以我绝对没有看到任务可以做任何线程不能做的事情。只是做的不一样。

使用线程,您有两种选择来停止它。 1. 在处理循环中让线程轮询以查看是否有标志请求取消,然后结束处理并让线程死亡。没问题。 2.调用Thread.Abort()(然后捕捉异常,做一个Join,担心Finally等)

这是线程中的数据库调用,所以一旦启动轮询将不起作用。

另一方面,如果您使用 TPL 和 CancellationToken,在我看来您仍在轮询然后创建异常。它看起来就像我在案例 1 中用线程描述的一样。一旦我开始该数据库调用(我还打算在它周围放置一个异步/等待),我就无法测试 CancellationToken 中的更改。就此而言,TPL 更糟糕,因为在 Db 读取期间调用 CancellationToken 将完全不执行任何操作,远不如 Thread.Abort() 执行的操作。

我不敢相信这是一个独特的问题,但我还没有找到真正的解决方案,我已经阅读了很多。无论是线程还是任务,工作线程都必须轮询以知道它应该停止然后停止(连接到 Db 时不可能。它不在循环中。)否则线程必须中止,抛出 ThreadAbortedException 或 TaskCanceledException .

我目前的计划是将每个作业作为一个长时间运行的线程启动。如果线程超过时间限制,我会调用Thread.Abort,在线程中捕获异常,然后在Abort()之后的线程上做Join()。

我非常非常愿意接受建议...谢谢,迈克

我会放这个链接,因为它声称可以这样做,但我无法弄清楚它并且没有回复让我认为它会起作用 multi-threading-cross-class-cancellation-with-tpl

哦,这看起来很有可能,但我也不知道Treating a Thread as a Service

【问题讨论】:

  • 如果数据库驱动程序可以接受CancellationToken,那么它可能能够监听它并优雅地取消请求。例如,在 SQL Management Studio 中,您可以取消正在运行的请求,因此 ADO.Net 驱动程序可以做到这一点并非不可能。它是否真的这样做是另一回事。如果你 Thread.Abort() 则它会抛出一个不可停止的异常,因此在这种情况下驱动程序可能会或可能不会取消。

标签: c# .net multithreading


【解决方案1】:

您不能实际上取消数据库操作。请求通过网络发送;它现在“在那里”,没有把它拉回来。您真正能做的最好的事情就是忽略返回的响应,并继续执行在操作实际完成后您将执行的任何代码。重要的是要认识到这是什么;这实际上并没有取消任何东西,它只是在继续,即使你还没有完成。这是一个非常重要的区别。

如果你有一些任务,并且你希望它在你想要的时候被取消,你可以创建一个使用 CancellationToken 的延续,这样当令牌指示它应该时,延续将被标记为已取消是,否则任务完成后就完成了。然后,您可以使用该延续的 Task 代替所有延续的实际基础任务,如果令牌被取消,该任务将被取消。

public static Task WithCancellation(this Task task
    , CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}
public static Task<T> WithCancellation<T>(this Task<T> task
    , CancellationToken token)
{
    return task.ContinueWith(t => t.GetAwaiter().GetResult(), token);
}

然后,您可以接受一个给定的任务,传入一个取消令牌,然后取回一个具有相同结果的任务,但取消语义有所改变。

【讨论】:

  • 好吧,这可能行得通,但你没有给我什么继续。一个是任务,另一个是带有 TResult 的任务。每人一行代码,因此只需一行代码。
  • @Miguelito 你要么有一个有结果的任务,要么你没有。您传入任一种类型的任务,然后 C# 的重载决议接管,使用适合您的上下文的任何一个。如果你知道你永远只需要一个,那么就使用那个。如果您认为这在将来可能有用并希望保留它,请保留两者,因为您不知道您未来的任务是否会有结果。您现在可以接受任何任务,世界上的任何任务,并在包装​​任务中提供您想要的任何所需的取消语义。
  • 我的意思是......好吧,你说得有道理,这可能会奏效,但你没有给我什么继续。一个是任务,另一个是带有 TResult 的任务。每人一行代码,所以真的只有一行代码可以使用。我的理解是 taskContinueWith “创建一个在目标任务完成时异步执行的延续”。好吧,目标任务永远不会完成。即使我调用令牌,第一个任务也永远不会知道或被取消。如果我错了,请扩大您的答案。我知道其他人将来会想要这个答案,因为阅读很多并没有显示出来。
  • @Miguelito 你说的很对,第一个任务永远不会知道续集被取消了。这就是我第一段的重点。您的任务所代表的固有操作是不可取消的。没有办法实际上取消它。如果有,那么您调用的生成任务的任何 API 都需要接受取消令牌。您可以做的最好的事情是创建一个新任务,当您这样说时将被取消,在任何地方使用该新任务而不是其他任务,因此当您“取消”此操作时允许任何其他延续继续。
  • Yesss... 但是虽然无法取消操作,但调用Db操作的Task仍在等待中。我必须让任务消失。 ......好吧,所以“创建一个新任务,当你这样说时将被取消”......啊,它失败后我不需要继续。我需要结束整个任务。没有来自 Db 的数据 = 没有进一步的工作要做。记录错误并转到下一个作业。我真的很想了解你,但我没有。
【解决方案2】:

您还有其他几种取消线程的选项。例如,您的线程可以进行异步数据库调用,然后等待该调用和取消令牌。例如:

// cmd is a SqlCommand object
// token is a cancellation token
IAsyncResult ia = cmd.BeginExecuteNonQuery();  // starts an async request

WaitHandle[] handles = new WaitHandle[]{token.WaitHandle, ia.AsyncWaitHandle};
var ix = WaitHandle.WaitAny(handles);
if (ix == 0)
{
    // cancellation was requested
}
else if (ix == 1)
{
    // async database operation is done. Harvest the result.
}

如果操作被取消,则无需抛出异常。并且不需要Thread.Abort

使用Task,这一切都变得更加清晰,但本质上是一样的。 Task 处理常见错误并帮助您更好地将所有部分组合在一起。

你说:

TPL 只是一个很好的线程包装器,所以我认为任务完全没有线程不能做的事情。只是做的不一样。

这是真的,就目前而言。毕竟,C# 只是汇编语言程序的一个很好的包装器,所以我绝对没有看到 C# 程序可以做我不能用汇编语言做的事情。但是用 C# 来做这件事要容易得多,也快得多。

TPL 或任务之间的区别以及管理您自己的线程也是如此。您可以做各种管理自己的线程的事情,或者您可以让 TPL 处理所有细节并更有可能正确处理。

【讨论】:

    猜你喜欢
    • 2012-11-06
    • 2013-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多