从 DoWork 处理程序调用方法?

Calling method from DoWork handler?

我正在尝试使用 BackgroundWorker 来完成任务。我已经让工作人员正确地 运行,在 DoWork 方法下它然后调用另一个执行的方法但后来我遇到了我的问题:当该方法试图调用另一个方法时它没有成功并且没有抛出一个异常,我只能认为这是我在 BackgroundWorker 上做错的事情,因为当 运行 在 UI 线程上测试方法按预期执行时。

这是我 运行 我的工人所在的地方:

private void btnAddShots_Click(object sender, EventArgs e)
{
    backgroundWorker.RunWorkerAsync();     
}

这是我的 DoWork 方法:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    int noOfShots = dataGridShots.Rows.Count - 1;
    int count = 0;

    while (count < noOfShots)
    {
        addTaskPair(dataGridShots.Rows[count].Cells[0].Value.ToString(), 
            dataGridShots.Rows[count].Cells[1].Value.ToString(), 
            dataGridShots.Rows[count].Cells[2].Value.ToString());

        count += 1;
    }
}

这是我的工作人员调用的 addTaskPair 方法的精简版本:

private void addTaskPair(string taskName, string taskDescription, string taskPriority)
{
    try
    {
        Task trackingTask = new Task();

        trackingTask.content = taskName;
        trackingTask.description = taskDescription;
        trackingTask.priority = taskPriority; 

        string trackingJson = JsonConvert.SerializeObject(trackingTask);
        trackingJson = "{ \"todo-item\":" + trackingJson + " }";

        string jsonResponse;
        jsonResponse = postJSON(trackingJson, teamworkURL + "/tasklists/" 
            + todoLists.todoLists[cmbTrackingList.SelectedIndex].id + "/tasks.json");
    }
    catch (Exception e)
    {
        debugMessage(e.ToString());
    }
}

在上面的示例中,您会看到我调用了方法 postJSON,这是我遇到困难的地方。通过测试我已经验证了上面的方法运行s,但是postJSON方法在这个线程中被调用时根本没有运行。

我在研究这个问题时看到了很多对 Invoking 的引用,但它们似乎都适用于更改 ui 控件,而我不需要这样做(尽管进度条是使用ProgressChanged BackgroundWorker 事件)。

如果需要,我可以进一步澄清我的问题,但我真的希望得到这方面的帮助,因为我以前从未成功地使用过 backgroundworker 或线程(我不是专业人士,我相信你可以从我的代码)。

您正在使用来自 BackgroundWorker.DoWork 事件处理程序的 UI 控件。不要那样做。

在启动 BackgroundWorker 之前收集数据,并将其作为参数传递给 RunWorkerAsync 方法。不要触摸 BackgroundWorker.DoWork 中的 UI - 通过 ReportProgress 方法更新进度是可以的。

此外,如果您 运行 使用 .NET 4.5+,您可能需要考虑改用新的 Task 模式。它仍然需要您事先收集数据进行处理,但它更容易使用:

(EDIT:正如 Peter 所建议的,无效访问发生在 cmbTrackingList.SelectedIndex 中;我已将其包含在下面的代码中。这正是我建议的原因对在单独线程中发生的操作使用 static 方法 - 这会让您对正在处理的数据有更多的思考)

var todoList = todoLists.todoLists[cmbTrackingList.SelectedIndex];

var data = 
  dataGridShots
  .Rows
  .Select
   (
     i => 
     new 
     { 
       TaskName = i.Cells[0].Value.ToString(), 
       TaskDescription = i.Cells[1].Value.ToString(), 
       TaskPriority = i.Cells[2].Value.ToString()
     }
   )
  .ToArray();

var result = 
   await Task.Run
   (
     () => 
     foreach (var row in data) 
       handleRowData(row.TaskName, row.TaskDescription, row.TaskPriority, todoList)
   );

既然你已经走到这一步,你可能会注意到让你的 postJson 方法异步也不难(有很多方法可以异步发出 HTTP 请求)- 这将允许您在不阻塞任何线程的情况下使整个代码异步。

多线程困难。始终尝试使用尽可能高的抽象,并避免任何共享状态。如果你 do 需要共享状态,你需要同步每个线程对它的每次访问 - 尽量避免这种情况(一个好的做法是让方法在不同的线程上执行static,这样你就不会不小心触及到共享状态)。

根据您的描述,DataGrid 组件的访问似乎不是 的问题。也就是说,这些语句似乎被正确执行,并且 addTaskPair() 方法被成功调用,但 postJSON() 方法没有。

鉴于此,我怀疑 cmbTrackingList.SelectedIndex 的评估是抛出异常并中断线程的原因。

也就是说,建议仍然是一样的:将与 UI 相关的内容保留在 UI 线程中,仅 运行 其他内容在 [=153= 之外] 线。鉴于您发布的代码,似乎唯一真正应该是异步的(即 运行 在后台,以免延迟 UI 线程太多)是调用至 postJSON()。据推测这是一个同步网络调用,因此可能需要一段时间。其他内容应该 运行 确定且快速。

鉴于 ,以下是我重构代码的方式,利用新的 async/await 功能:

private async void btnAddShots_Click(object sender, EventArgs e)
{
    int noOfShots = dataGridShots.Rows.Count - 1;
    int count = 0;

    while (count < noOfShots)
    {
        await addTaskPair(dataGridShots.Rows[count].Cells[0].Value.ToString(), 
            dataGridShots.Rows[count].Cells[1].Value.ToString(), 
            dataGridShots.Rows[count].Cells[2].Value.ToString());

        count += 1;
    }
}

private async Task addTaskPair(string taskName, string taskDescription, string taskPriority)
{
    try
    {
        TaskData trackingTask = new TaskData();

        trackingTask.content = taskName;
        trackingTask.description = taskDescription;
        trackingTask.priority = taskPriority; 

        string trackingJson = JsonConvert.SerializeObject(trackingTask);
        trackingJson = "{ \"todo-item\":" + trackingJson + " }";

        string jsonResponse;
        string url = teamworkURL + "/tasklists/" 
            + todoLists.todoLists[cmbTrackingList.SelectedIndex].id + "/tasks.json";
        jsonResponse = await Task.Run(() => postJSON(trackingJson, url));
    }
    catch (Exception e)
    {
        debugMessage(e.ToString());
    }
}

注意:在上面我把你自己的名字Task改成了TaskData。我 强烈 建议您选择 Task 以外的名称,因为在整个现代 .NET API 中普遍使用 .NET Task 类型.

在上面,大部分代码会在UI线程上运行。 async 方法在任何 await 语句中被编译器重写为 return,并在等待的 Task 完成时恢复执行该方法。请注意,当最终有一个 Task 对象等待时,async 方法仅 returns;所以在上面,一旦 addTaskPair() 方法调用了 Task.Run() 并且它本身在 await 处有 returned,btnAddShots_Click() 方法最初将 return声明。

重要:在此上下文中,从 UI 线程调用和等待异步方法会导致框架 运行 方法的其余部分 返回到 UI线程。也就是说,当异步操作完成后,代码执行的控制权 return 返回到您开始的 UI 线程。

正是这个功能使所有这些功能都能正常工作,因此确保您理解它很有用。 :)

postJSON() 的调用在单独的线程中执行,使用由 Task.Run() 方法创建的 Task 对象。由于它将在 UI 线程以外的其他线程上执行,因此我已将其 URL 参数的计算移动到局部变量,就在调用 Task.Run() 之前,然后传递该变量在任务线程中调用 postJSON() 方法。这样做可以确保 cmbTrackingList.SelectedIndex 的计算在 UI 线程中完成。


编辑:

注意到 OP 评论说他使用的是 .NET 4 而不是 4.5(其中正式发布了 async/await 功能),我提供了这个稍微尴尬的替代方案,仍然保留了上述 4.5 兼容版本的执行特性。虽然可以在 VS2010 上安装 async/await 功能(恕我直言,这是一种更好的方法),但这种替代方法允许 "pure" .NET 4 代码,同时仍然实现基本相同的 运行时间结果。

private void btnAddShots_Click(object sender, EventArgs e)
{
    Action<Task> continuation = null;
    TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    int noOfShots = dataGridShots.Rows.Count - 1;
    int count = 0;

    // Note that the continuation delegate is chained, attaching itself as
    // the continuation for each successive task, thus achieving a looping
    // mechanism.
    continuation = task =>
    {
        if (count < noOfShots)
        {
            addTaskPair(dataGridShots.Rows[count].Cells[0].Value.ToString(), 
                dataGridShots.Rows[count].Cells[1].Value.ToString(), 
                dataGridShots.Rows[count].Cells[2].Value.ToString())
                .ContinueWith(continuation, uiScheduler);

            count += 1;
        }
    }

    // Invoking the continuation delegate directly gets the ball rolling
    continuation(null);
}

private Task addTaskPair(string taskName, string taskDescription, string taskPriority)
{
    try
    {
        TaskData trackingTask = new TaskData();

        trackingTask.content = taskName;
        trackingTask.description = taskDescription;
        trackingTask.priority = taskPriority; 

        string trackingJson = JsonConvert.SerializeObject(trackingTask);
        trackingJson = "{ \"todo-item\":" + trackingJson + " }";

        string url = teamworkURL + "/tasklists/" 
            + todoLists.todoLists[cmbTrackingList.SelectedIndex].id + "/tasks.json";

        // NOTE: must explicitly specify TaskScheduler.Default, because
        // the default scheduler in the context of a Task is whatever the
        // current scheduler is, which while executing a continuation would
        // be the UI scheduler, not TaskScheduler.Default.
        return Task.Factory.StartNew(() => postJSON(trackingJson, url),
            CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default)
            .ContinueWith(task =>
            {
                if (task.Exception != null)
                {
                    // Task exceptions are wrapped in an AggregateException
                    debugMessage(task.Exception.InnerException.ToString());
                }
                else
                {
                    string jsonResponse = task.Result;

                    // do something with jsonResponse?
                }
            }, TaskScheduler.FromCurrentSynchronizationContext());
    }
    catch (Exception e)
    {
        debugMessage(e.ToString());
    }
}

备注:

  • 当前同步上下文的任务计划程序用于每个延续。这确保延续本身在 UI 线程上执行,您可以在其中安全地与 UI 对象交互。
  • 恕我直言,最尴尬的部分是您的 while 循环变成了 if 语句,因为循环的执行跨越多个方法调用。
  • 虽然 async/await 通常允许正常的异常处理语法,但在使用显式延续时您没有此选项。但是 Task 将包装在 AggregateException 实例中发生的任何异常,您可以使用它来获取真正的异常并报告它。
  • TaskFactory.StartNew() 有一些微妙的行为:它用于 运行 给定任务的调度程序是 TaskScheduler.Current。第一次调用 addTaskPair() 方法时,没有当前任务,因此 TaskScheduler.Current return 是默认(即线程池)调度程序。但是每次后续调用 addTaskPair() 方法时,它都是从任务延续中调用的,因此 TaskScheduler.Current 将 return 用于执行延续的调度程序。当然,我们有意将其设为 UI 调度程序,而 运行 使用该调度程序来执行新的 postJSON() 任务将达不到目的,因为它只会在当前线程上同步执行。所以这里势在必行指定我们想要的调度器,即TaskScheduler.Default,对应线程池调度器

如果没有 async/await,就很难把事情做好。它在语法上更冗长,但恕我直言,它仍然是一个相当不错的选择,因为它基本上保留了所需的命令式代码结构。特别是,您可以在 UI 线程中保持执行流程,使 UI 对象的访问变得微不足道,并且仅在需要时分叉 long-运行ning 操作.

(我还应该指出,这个 .NET 4 版本严格来说并不是编译器在使用 async/await 时为您生成的版本。它非常相似,但不完全是相同。另外,我要指出的是,虽然以这种方式使用单个 await 实现一个方法还不错,但如果您希望在同一方法中进行多个延续,它会有点失控。这是可能的,但是那时我认为升级到最新版本的 C# 的冲动会非常引人注目 :))。


终于……

如果在完成上述所有操作后,您想坚持使用 BackgroundWorker,应该可以通过对代码进行相对简单的更改来避免发生的异常:

    int selectedIndex = (int)Invoke((Func<int>)(() => cmbTrackingList.SelectedIndex));

    jsonResponse = postJSON(trackingJson, teamworkURL + "/tasklists/" 
        + todoLists.todoLists[selectedIndex].id + "/tasks.json");

即只需使用 Control.Invoke() 方法在 UI 线程上调用一个匿名方法,该方法将 return cmbTrackingList.SelectedIndex 的值。 Control.Invoke() 方法将接收 returned 值,然后 return 将其返回给您。由于 Control.Invoke() 是通用的,因此它的 object return 类型必须转换为您知道正在 returned 的类型。

这确保 cmbTrackingList 对象仅在 UI 线程上被访问。我还会注意到,如果在后台处理正在进行时此索引预计不会更改(或者特别是 not 应该),那么另一种选择是检索值在您的 btnAddShots_Click() 方法中,然后将其传递给 DoWork 事件处理程序,后者又会将其传递给需要它的 addTaskPair() 方法。

我把这个选项放在最后是因为我真的相信学习 async/await 功能是重要和值得的,而 BackgroundWorker class 已经服务多年来,我们基本上已经被新功能弃用了。但我也欣然承认 BackgroundWorker 仍然是一种很好的做事方式,并且可以在您的场景中发挥作用。