【问题标题】:C# Threading using invoke, freezing the formC#线程使用invoke,冻结表单
【发布时间】:2013-01-28 19:19:14
【问题描述】:

我正在尝试使用线程并防止程序在线程忙时冻结。它应该显示进度(写入 0/1),而不仅仅是在完成后显示结果,同时冻结表单。

在当前程序中,我正在尝试写入文本框,并且实际上看到了持续的进度,并且表单不会受到其他线程的任务的影响。

我现在拥有的是我可以使用调用写入带有线程的文本框,但它只显示结果(线程忙时表单冻结),并且表单冻结。

表格图片:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace MultiThreading
{
public partial class MultiThreading : Form
{
    public MultiThreading()
    {
        InitializeComponent();
    }

    Thread writeOne, writeTwo;

    private void writeText(TextBox textBox, string text)
    {
        if (textBox.InvokeRequired)
        {
            textBox.BeginInvoke((MethodInvoker)delegate()
            {
                for (int i = 0; i < 500; i++)
                {
                    textBox.Text += text;
                }
            });
        }
        else
        {
            for (int i = 0; i < 500; i++)
            {
                textBox.Text += text;
            }
        }
    }
    private void btnWrite1_Click(object sender, EventArgs e)
    {
        writeOne = new Thread(() => writeText(txtOutput1, "0"));
        writeOne.Start();
    }

    private void btnWrite2_Click(object sender, EventArgs e)
    {
        writeTwo = new Thread(() => writeText(txtOutput2, "1"));
        writeTwo.Start();
    }

    private void btnClear1_Click(object sender, EventArgs e)
    {
        txtOutput1.Clear();
    }

    private void btnClear2_Click(object sender, EventArgs e)
    {
        txtOutput2.Clear();
    }

    private void btnWriteBoth_Click(object sender, EventArgs e)
    {
        writeOne = new Thread(() => writeText(txtOutput1, "0"));
        writeTwo = new Thread(() => writeText(txtOutput2, "1"));

        writeOne.Start();
        writeTwo.Start();
    }

    private void btnClearBoth_Click(object sender, EventArgs e)
    {
        txtOutput1.Clear();
        txtOutput2.Clear();
    }
}

}

编辑:

顺便说一句,对于任何想知道的人,我是多线程的新手,我只是想编写一个小程序来了解执行此操作的最佳方法。

我知道我之前的调用并没有真正帮助,因为我仍然没有给表单更新的机会,所以它到了那里。

好的,像这样运行 1 个线程可以工作,但仍然同时运行多个线程,在线程完成之前不会更新表单。
我添加了一个 thread.sleep() 以便我可以在写作时尝试清除,看看我是否仍然可以使用该表单。

写入 1 个文本框时,我仍然可以在写入时清除屏幕。
但是一旦我使用了 2 个线程,我就不能再使用表单了,直到线程完成并给出输出。

private void writeText(TextBox textBox, string text)
    {
        for (int i = 0; i < 500; i++)
        {
            Invoke(new MethodInvoker(() =>
            {
                textBox.Text += text;
                Thread.Sleep(2);
            }));
        }

    }

(如果我在这方面完全错了,我不介意阅读一些示例/线程,除了后台工作人员之外,我仍在尝试了解执行此操作的最佳方法)

编辑2:

我通过减少写入量来减少调用次数,但增加延迟会产生与持续写入相同的效果,只是减少了负载。

private void writeText(TextBox textBox, string text)
    {
        for (int i = 0; i < 500; i++)
        {
            Invoke(new MethodInvoker(() =>
            {
                textBox.Text += text;
                Thread.Sleep(2);
            }));
        }

    }

编辑 3:

Sumeet 的示例使用

Application.DoEvents();

(注意 s,.DoEvent 不起作用,可能是拼写错误:P),同时编写多个字符串并让它们显示进度而不仅仅是结果。

所以代码再次更新:)

*使用新按钮创建 5 个线程,将随机数写入两个文本框

private void writeText(TextBox textBox, string text)
    {
        for (int i = 0; i < 57; i++)
        {
            Invoke(new MethodInvoker(() =>
            {
                textBox.Text += text;
                Thread.Sleep(5);
                Application.DoEvents();
            }));
        }

    }
private void btnNewThread_Click(object sender, EventArgs e)
    {
        Random random = new Random();
        int[] randomNumber = new int[5];
        for (int i = 0; i < 5; i++)
        {
            randomNumber[i] = random.Next(2, 9);
            new Thread(() => writeText(txtOutput1, randomNumber[i-1].ToString())).Start();
            new Thread(() => writeText(txtOutput2, randomNumber[i-1].ToString())).Start();
        }
    }

【问题讨论】:

  • 如果您有一个长时间运行的进程在后台运行,并且 UI 需要保持响应,请考虑使用 BackgroundWorker
  • @RobertHarvey 这对这个特定的例子没有帮助。
  • @Servy 这个特殊的例子似乎......坏了。并且做作。这种事情有一条老路。
  • @RobertHarvey 我不同意。它代表了不同类型的问题;长时间运行的 UI 操作,您需要在其中执行大量 UI 更新,而不是长时间运行的非 UI 任务,该任务恰好更新 UI 以更新进度或完成时。这是一个有效的用例,但 backgroundworker 类并非旨在处理。
  • 我相信你们都是对的。正如Servy 指出的那样,BackgroundWorker 不适合这项任务。最快的解决方法可能是在for 循环中移动BeginInvoke 调用。然而,这个例子是人为的;我想不出一个我会认为这是一个“有效”用例的现实世界情况。这几乎肯定表明需要重新考虑您的设计。

标签: c# multithreading invoke


【解决方案1】:

此解决方案有效!已经检查过了。

问题是您一直告诉 UI 线程更改文本,但从不让它有时间向您显示更新的文本。 要使您的 UI 显示更改后的文本,请像这样添加 Application.DoEvents 行:

textBox.Text += text;
Application.DoEvents();

附言:删除 If / Else 循环的 else 块,这是多余的,而且正如其他人所指出的那样,创建这 2 个线程没有任何用处,因为他们所做的只是在 UI 线程本身上发布消息。

【讨论】:

  • 这确实有效,现在线程没有延迟,甚至运行多个线程;)
  • 我会谨慎对待DoEvents()。谨慎使用它;我见过人们不分青红皂白地使用它使程序陷入僵局。如果你的线程正确,你应该很少需要DoEvents()
  • 如果你问我,你应该永远使用Application.DoEvents()。它......在这种情况下是可以管理的,但使用它来代替适当的多线程并不是你应该养成的习惯。
  • 这不会使应用程序多线程,它只是推送 UI 更新。总体 UX 充其量仍会不稳定 - 尝试在流程进行时移动表单
  • 恕我直言,随心所欲地使用 Application.DoEvents(),不仅完全安全(如果您遇到死锁,请归咎于您的线程而不是 Application.DoEvents 并且无论如何您都会死锁。),但是对于能够在没有实际产生线程的情况下进行伪线程的快速安全窗口窗体来说,它也更容易,因此不用担心陷入死锁。
【解决方案2】:

您仍在执行单线程任务,如果需要,只需在 UI 线程上重新启动即可。

for (int i = 0; i < 500; i++){
    string text = ""+i;
    textBox.BeginInvoke((MethodInvoker)delegate()
            {
                textBox.Text += text;
            });
}

【讨论】:

    【解决方案3】:

    问题是您正在启动一个新线程,然后该新线程什么都不做,只是为 UI 线程添加一个新任务以处理大量工作。为了让您的表单保持响应,您需要留出时间让 UI 线程什么都不做,或者至少不要花费大量时间来执行任何一项任务。

    为了保持表单响应,我们需要有很多小的BeginInvoke(或Invoke)调用。

    private void writeText(TextBox textBox, string text)
    {
        for (int i = 0; i < 500; i++)
        {
            Invoke(new MethodInvoker(() =>
            {
                textBox.Text += text;
            }));
        }
    }
    

    通过大量的小调用调用,它允许在操作过程中处理诸如绘制事件、鼠标移动/单击事件等。另请注意,我删除了 InvokeRequired 调用。我们知道这个方法会被一个非 UI 线程调用,所以不需要它。

    【讨论】:

      【解决方案4】:

      你违背了使用线程的目的。

      你的线程所做的只是告诉UI线程使用BeginInvoke()执行一些代码。

      所有实际工作都发生在 UI 线程上。

      【讨论】:

      • 这解释了问题,但没有讨论如何解决它。
      【解决方案5】:

      您要么进行数据处理,要么只是尝试为 UI 设置动画。

      对于数据处理,您应该在后台线程上完成所有繁重的工作,并且只偶尔更新 UI。在您的示例中,TextBox 在这方面特别麻烦,因为您要向底层数据模型添加数百次数据,并且每次呈现 UI 元素(TextBox)需要更长的时间。 您必须注意更新 UI 的频率,以便 UI 更新的处理不会压倒数据模型更新。像这样的文本框很讨厌。

      在下面的示例中,在绘制事件期间设置的标志可确保在 TextBox 完成绘制最后一次更新之前,其他 UI 更新不会排队:

      string str = string.Empty;
      public void DoStuff()
      {
          System.Threading.ThreadPool.QueueUserWorkItem(WorkerThread);
      }
      
      void WorkerThread(object unused)
      {
          for (int i = 0; i < 1000; i++)
          {
              str += "0";
              if (updatedUI)
              {
                  updatedUI = false;
                  BeginInvoke(new Action<string>(UpdateUI), str);
              }
          }
          BeginInvoke(new Action<string>(UpdateUI), str);
      }
      
      private volatile bool updatedUI = true;
      void textbox1_Paint(object sender, PaintEventArgs e) // event hooked up in Form constructor
      {
          updatedUI = true;
      }
      
      void UpdateUI(string str)
      {
          textBox1.Text = str;
      }
      

      另一方面,如果 UI 动画是您的目标,那么您可能应该使用除 TextBox 之外的其他东西。它只是不适合如此频繁地处理更新。您可以针对特定用例对文本呈现进行一些优化。

      【讨论】:

      • 现在我只是想对多线程有一个很好的概述。我使用文本框是因为很容易看出您一直在向文本框写信,而且我仍然可以使用该表单。现在我只是想不断地写到文本框,在那里你可以看到进度,同时也做其他任务,而不是启动一个线程,只看到结果并且不能使用表单.
      【解决方案6】:

      您绝不能在大容量应用程序中使用字符串。用户界面与否。多线程与否。

      您应该使用 StringBuilder 来累积字符串。然后赋值

      tb.Text = sb.ToString();
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-10-05
        • 1970-01-01
        • 1970-01-01
        • 2021-12-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多