【问题标题】:Backgroundworker blocks UIBackgroundworker 阻止 UI
【发布时间】:2015-01-06 00:19:15
【问题描述】:

我尝试在另一个后台线程中执行一项简单的任务,因此 UI 不会被阻塞,但它仍然会被阻塞。我是不是忘了什么?

public partial class backgroundWorkerForm : Form
{
    public backgroundWorkerForm()
    {
        InitializeComponent();
    }

    private void doWorkButton_Click(object sender, EventArgs e)
    {
        if (backgroundWorker.IsBusy != true)
        {
            // Start the asynchronous operation.
            backgroundWorker.RunWorkerAsync();
        }
    }

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        //BackgroundWorker worker = sender as BackgroundWorker;
        if (textBoxOutput.InvokeRequired)
        {
            textBoxOutput.Invoke(new MethodInvoker(delegate
            {
                for (int i = 0; i < 10000; i++)
                {
                    textBoxOutput.AppendText(i + Environment.NewLine);
                }
            }));
        }
    }
}

当文本框被填满时,用户界面被阻塞:

【问题讨论】:

  • 难怪,你有循环在你的 Invoke 中完成工作(在 UI 线程上被调用)
  • 您的后台工作人员唯一要做的就是将整个循环分派到 UI 线程并在那里执行。
  • textBoxOutput.Invoke 将在 UI 线程中同步执行委托,因此它会阻塞。目前尚不清楚您要实现什么目标,但显然您根本不需要BackgroundWorker
  • 我认为您需要将后台工作处理和 UI 更新分开。为什么不能使用 OnRunWorkerCompleted 进行 UI 更新?
  • 一个想法是在内存中生成字符串并更新一次到TextBox。这会表现得更好,但在不知道你想要实现什么的情况下很难回答。

标签: c# winforms backgroundworker


【解决方案1】:

您的应用希望从后台线程重复向 UI 发送更新。为此有一个内置机制:后台工作人员的ProgressChanged 事件。 ReportProgress 调用在后台触发,但在 UI 线程上执行。

不过,我确实改变了一件事。过多的跨线程调用会降低性能。因此,我不是每次迭代都发送更新,而是将它们分成 100 个。

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        const int maxIterations = 10000;
        var progressLimit = 100;
        var staging = new List<int>();
        for (int i = 0; i < maxIterations; i++)
        {
            staging.Add(i);
            if (staging.Count % progressLimit == 0)
            {
                // Only send a COPY of the staging list because we 
                // may continue to modify staging inside this loop.
                // There are many ways to do this.  Below is just one way.
                backgroundWorker1.ReportProgress(staging.Count, staging.ToArray());
                staging.Clear();
            }
        }
        // Flush last bit in staging.
        if (staging.Count > 0)
        {
            // We are done with staging here so we can pass it as is.
            backgroundWorker1.ReportProgress(staging.Count, staging);
        }
    }

    // The ProgressChanged event is triggered in the background thread
    // but actually executes in the UI thread.
    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        if (e.ProgressPercentage == 0) return;
        // We don't care if an array or a list was passed.
        var updatedIndices = e.UserState as IEnumerable<int>;
        var sb = new StringBuilder();
        foreach (var index in updatedIndices)
        {
            sb.Append(index.ToString() + Environment.NewLine);
        }
        textBoxOutput.Text += sb.ToString();
    }

编辑:

这需要您将后台工作人员的 WorkerReportsProgress 属性设置为 true。

通过 ReportProgress 调用传递计数并不重要。我这样做只是为了得到一些东西并快速检查我是否可以返回。

确实应该记住有多少事件被调用和排队。您的原始应用程序有 10,000 个跨线程调用和 10,000 个 textBoxOutput 更改的文本事件。我的示例使用 100 个跨线程调用,因为我使用的页面大小为 100。我仍然可以为文本框生成 10,000 个更改的文本事件,而是使用 StringBuilder 对象来保存一整页的更改,然后为此更新一次文本框页。这样文本框只有 100 个更新事件。

编辑 2

您的应用是否需要分页并不是主要问题。最大的收获应该是后台工作人员在尝试将信息传达回 UI 时确实应该使用ReportProgress。请参阅此MSDN Link。特别值得注意的是:

您必须小心不要在其中操作任何用户界面对象 你的 DoWork 事件处理程序。相反,与用户界面进行通信 通过 ProgressChanged 和 RunWorkerCompleted 事件。

【讨论】:

  • 这似乎很好,对于我发布的这个特定的测试程序,但在我的实际应用程序中我不能使用这种方法,因为我不知道会发生多少更新事件。
  • 使用我的方法 (1) 后台工作人员不会阻塞 UI,以及 (2) 最小化跨线程调用以及文本框 TextChanged 调用,特别是在您不知道有多少更新事件的情况下会发生。这怎么没有帮助?
  • 我没有(也不能有)变量:maxIterationsprogressLimit,我也不需要报告任何进展。它可能对我发布的这段特别有用的代码有所帮助,但我无法将您的方法调整到我的应用程序中。无论如何,我会给你一个 +1 的努力,谢谢
【解决方案2】:

您的调用代码应该在循环之外。调用的代码块中的所有内容都将在 UI 线程上执行,从而阻塞它。

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        //BackgroundWorker worker = sender as BackgroundWorker;
        for (int i = 0; i < 10000; i++)
        {
            // do long-running task

            //if (textBoxOutput.InvokeRequired)
            //{
                textBoxOutput.Invoke(new MethodInvoker(delegate
                {
                    textBoxOutput.AppendText(i + Environment.NewLine);
                }));
            //}
        }
    }

【讨论】:

  • 不知道为什么这被赞成。这段代码不会改进任何东西,反而会使情况变得更糟。 -1
  • @sriram-sakthivel:你应该试试这个代码。它不再阻塞用户界面。
  • 出于兴趣,你为什么在循环内调用InvokeRequired?您认为每次循环迭代之间会发生什么变化?
  • 可能是newer version of operating systems does this,但是这段代码仍然使用UI线程来更新。不适用于所有操作系统
【解决方案3】:

一种更简单的方法是完全创建您的输出文本,然后将完整的输出粘贴到 TextBox 中,然后您只需要一次调用

protected delegate void SetTextDelegate(TextBox tb, string Text);

protected void SetText(TextBox tb, string Text)
{
    if (tb.InvokeRequired) {
        tb.Invoke(new SetTextDelegate(SetText), tb, Text);
        return;
    }
    tb.Text = Text;
}

然后在你的工作中

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    StringBuilder sb = new StringBuilder();
    //BackgroundWorker worker = sender as BackgroundWorker;
    for (int i = 0; i < 10000; i++)
    {
         sb.AppendLine(i.ToString());
    }
    SetText(textBoxOutput, sb.ToString());
}

【讨论】:

  • 这会发生什么变化以及如何回答这个问题?
  • @SriramSakthivel ,如果你在我的代码中 Thread.Sleep(100);AppendText 之后放置一个 Thread.Sleep(100);,UI 将被阻止,我必须等到所有 10000 个数字都被添加,但是使用这个代码不阻塞
  • 尝试运行您的代码并查看 UI 是否阻塞。这段代码以相当复杂的方式完成了 OP 所做的事情。
  • @SriramSakthivel 改变的是它被调用了 10000 次(2 个线程),因此它应该运行更顺畅,在 OP 开始帖子中,整个循环在一次调用中执行(因此UI 线程没有机会更新)
  • 这段代码无论如何都不会帮助 OP。这仍然会在 UI 线程中调用 10000 次迭代。老实说,由于 10000 次 Invoke 调用,这将比 OP 的代码花费更多的时间。
猜你喜欢
  • 2016-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-29
  • 2022-12-29
  • 1970-01-01
相关资源
最近更新 更多