【问题标题】:What's the best way to cancel a long operation?取消长时间操作的最佳方法是什么?
【发布时间】:2013-12-24 15:48:04
【问题描述】:

我遇到了一个不知道如何解决的问题。我有一个方法,其中包含来自填充数据表的服务的调用,例如:

private void GetCases()
{
    try
    {
        //Setup our criteria
        Web_search_criteria myCriteria = new Web_search_criteria()
        {
            Keynum = 9, //Use Opening Date Key
            Range_start = "20100101", //01-01-2010
            Range_end = "20121223" //12-23-2013
        };


        //The myCases object is a datatable that is populated from the GetCasesDT() call.
        int status = db.server.GetCasesDT(myCriteria, ref myCases);
    }
    catch (Exception ex)
    {
        XtraMessageBox.Show("Unable to get data: " + ex.Message);
    }
}

所以,如您所见,我无法一次只抓取几个箱子 - 它只是抓取所有箱子。

现在,我在 BackgroundWorker 的 DoWork 事件中调用了这个方法,并且我显示了另一个带有选取框进度条的表单,以便用户知道系统实际上正在执行某些操作。在该表单上,我有一个订阅的取消按钮。这是执行此操作的代码:

    backgroundWorker1 = new BackgroundWorker() 
    { 
        WorkerSupportsCancellation = true,
        WorkerReportsProgress = true
    };

    //DoWork Event
    backgroundWorker1.DoWork += backgroundWorker1_DoWork;

    //Show the progress bar with subscription to event
    pb.btnCancel.Click += this.CancelBW;
    pb.Show();

    //Run the backgroundworker
    this.backgroundWorker1.RunWorkerAsync();

    //Don't lock up the application
    while (this.backgroundWorker1.IsBusy)
    {
        Application.DoEvents();
    }

我曾尝试使用 CancelAsync() 来取消 CancelBW 事件中的 BackgroundWorker,但后来我阅读了更多内容并意识到这不起作用,并且只有在我可以中断初始调用以便 BackgroundWorker 可以检查时才会起作用进展。

我曾考虑过使用线程而不是 BackgroundWorker,但读到中止线程会导致小猫自燃。

那么,对于我来说,处理这个用户可能会取消一个漫长的过程的最佳方法是什么?

【问题讨论】:

  • +1 表示小猫自燃。 (现在有一条我从未想过会放到互联网上的台词。)
  • 您使用的是什么版本的 .NET/C#?如果是 .NET 4.0/4.5,您可以尝试使用包含取消令牌支持的任务。
  • @ledbutter 感谢您的建议。我使用的是 4.0,所以我将看一下 Tasks 类,看看它是如何工作的。
  • 深思熟虑:这可能与您的应用程序的目标背道而驰,并且肯定会改变用户体验,但您也可以考虑分页。如果他们给出的日期范围大于 XX 天,您只需请求 30 天,并使用“更多”或“下一步”按钮来获取更多数据。 或者 可以在完成后自动链接 30 天的请求,因此当您升级到完整的 3 年数据时,您会立即显示数据。不是您问题的答案,而是另一种看待大局的方式。

标签: c# multithreading process backgroundworker


【解决方案1】:

没有正确取消此操作的基本方法。这不是 CPU 绑定的工作,而是网络绑定的工作。您发送了请求,您不能完全从互联网上提取该请求。您真正能做的就是在满足取消条件时让您的代码继续执行,而不是等待操作完成。

您的应用程序的一个主要问题是您抽出消息队列的忙循环:

while (this.backgroundWorker1.IsBusy)
{
    Application.DoEvents();
}

这通常是个坏主意,应该避免这种做法。首先使用BackgroundWorker 的想法是让它是异步的;在 BGW 完成之前,您不应该尝试阻止当前方法。

虽然有一些方法可以将 BGW 纳入其中;这种特殊情况使用任务并行库可能更容易解决。

var cts = new CancellationTokenSource();

pb.btnCancel.Click += (s, e) => cts.Cancel();
pb.Show();

var task = Task.Factory.StartNew(() => GetCases())
    .ContinueWith(t => t.Result, cts.Token)
    .ContinueWith(t =>
    {
        //TODO do whatever you want after the operation finishes or is cancelled;
        //use t.IsCanceled to see if it was canceled or not.
    });

(我还建议重构GetCases,使其返回从数据库获取的DataTable,而不是修改实例字段。然后您可以通过任务的Result 访问它。

【讨论】:

  • @user1949119 你遇到了什么问题?在不知道它们是什么的情况下,我无话可说。
  • 非常感谢您的解释和示例代码。不过,我遇到了 t.Result 的问题-“System.Threading.Tasks.Task”不包含“Result”的定义,并且没有扩展方法“Result”接受“System.Threading.Tasks”类型的第一个参数.Task' 可以找到(您是否缺少 using 指令或程序集引用?)
  • @user1949119 见我的最后一段。我建议GetCases 应该返回一个结果,这意味着任务将有一个结果。如果你不这样做,它就没有结果,所以你不需要使用它。虽然您可以只取出使用结果,但首选的解决方案是通过修改GetCases来确保任务有结果。
  • 另外,TPL 在 C# 5 中支持 async 和 await,这确实有助于清理代码
  • @user1949119 您可能从某个地方阻塞了 UI 线程。它不在您显示的代码中,它可能在其他地方。您在应该异步执行操作的某个时间阻塞。
【解决方案2】:

如果您不想中止线程(这确实会导致世界崩溃和小猫燃烧),结束线程的唯一合理方法是细化其中的工作并经常检查取消标记(以您喜欢的任何形式:内置 [for the TPL]、消息、锁定变量、信号量或其他)并“干净地”结束线程。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-03
    • 2011-10-06
    相关资源
    最近更新 更多