【问题标题】:In C#, wait on the mainthread while continuing to process UI updates? (.NET 2.0 CF)在 C# 中,在继续处理 UI 更新的同时等待主线程? (.NET 2.0 CF)
【发布时间】:2010-09-26 12:33:52
【问题描述】:

我想以其他方式阻止主线程上的代码执行,同时仍允许显示 UI 更改。

我试图想出一个简化的示例版本来说明我正在尝试做的事情;这是我能想到的最好的。显然,它不能证明我想要的行为,否则我不会发布问题。我只是希望它能提供一些代码上下文来支持我对我希望解决的问题的糟糕解释。

在表单上的按钮单击处理程序中,我有这个:

    private void button2_Click(object sender, EventArgs e)
    {
        AutoResetEvent autoResetEvent = new AutoResetEvent(false);

        new Thread(delegate() 
        {
            // do something that takes a while.
            Thread.Sleep(1000);

            // Update UI w/BeginInvoke
            this.BeginInvoke(new ThreadStart(
                delegate() { 
                    this.Text = "Working... 1";
                    this.Refresh();
                    Thread.Sleep(1000); // gimme a chance to see the new text
                }));

            // do something else that takes a while.
            Thread.Sleep(1000);

            // Update UI w/Invoke
            this.Invoke(new ThreadStart(
                delegate() {
                    this.Text = "Working... 2";
                    this.Refresh();
                    Thread.Sleep(1000); // gimme a chance to see the new text
                }));

            // do something else that takes a while.
            Thread.Sleep(1000);

            autoResetEvent.Set();
        }).Start();


        // I want the UI to update during this 4 seconds, even though I'm 
        // blocking the mainthread
        if (autoResetEvent.WaitOne(4000, false))
        {
            this.Text = "Event Signalled";
        }
        else
        {
            this.Text = "Event Wait Timeout";
        }
        Thread.Sleep(1000); // gimme a chance to see the new text
        this.Refresh();
    }

如果我没有在 WaitOne() 上设置超时,应用程序将在 Invoke() 调用上死锁。


至于我为什么要这样做,我的任务是移动应用程序的一个子系统以在后台线程中工作,但它仍然只有时会阻塞用户的工作流(主线程)仅与该子系统相关的某些类型的工作。

【问题讨论】:

    标签: c# multithreading


    【解决方案1】:

    您想使用“BackgroundWorker”类,它将为您消除大部分痛苦。但如前所述,您还需要构建它以便主线程更新UI 和工作人员正在做繁重的工作。

    【讨论】:

      【解决方案2】:

      这比你想象的要容易。

      建议:当你需要一个线程来执行一些偶尔的工作时,从线程池中获取它,这样你就不需要奇怪/容易出错的回收代码了。

      当您希望另一个线程上的某些内容更新您的 UI 时,您只需要对表单的引用并调用 Form.Invoke 传递您希望主线程执行的 UI 代码;在任何情况下,最好的做法是尽快释放 UI 线程。

      即:

      private void button1_Click(object sender, EventArgs e)
      {
          // this is the UI thread
      
          ThreadPool.QueueUserWorkItem(delegate(object state)
          {
              // this is the background thread
              // get the job done
              Thread.Sleep(5000);
              int result = 2 + 2;
      
              // next call is to the Invoke method of the form
              this.Invoke(new Action<int>(delegate(int res)
              {
                  // this is the UI thread
                  // update it!
                  label1.Text = res.ToString();
              }), result);
          });
      }
      

      希望对你有帮助:)

      编辑:对不起,我没有阅读“阻止用户工作流程”部分。

      WindowsForms 不是为此而设计的,阻塞主线程是不好的(它处理来自操作系统的消息)。

      您不必通过冻结表单来阻止用户工作流(Windows 会认为它“不响应”),阻止用户工作流的方法是禁用您想要的任何控件(使用上面的 Invoke 方法如果来自另一个线程),甚至是整个表单!

      【讨论】:

        【解决方案3】:

        “阻塞”主线程的常见活动是打开消息框或模式对话框。主代码似乎在 MessageBox 或 ShowDialog 调用处阻塞。

        这些项目的工作方式(MessageBox 只是一个专门的模式对话框)是它们在阻塞时包含自己的消息泵。

        虽然这是一个令人讨厌的 hack,但您可以通过循环调用 Application.DoEvents() 在您的应用程序中执行类似的操作,以在您等待其他任务完成时保持用户消息不断涌现。您需要小心,因为像这样发送消息可能会导致各种讨厌的事情 - 例如有人关闭表单或重新输入您当前的消息处理程序 - 模式对话框通过有效地禁用启动它们的表单的输入来避免这种情况。

        我的意思是说 BackgroundWorker 是一个更好的解决方案,如果你可以使它适合的话。我有时将它与模式“进度对话框”结合起来,为我提供后台线程/消息泵送和 UI 线程的阻塞。

        编辑 - 扩展最后一位:

        我使用的一种方法是创建一个“进度表单”类,该类将 BackgroundWorker 对象作为构造函数参数,并包含传递给它的后台工作程序的进度和完成事件的处理程序。

        希望完成工作的表单创建后台工作人员并连接“工作”事件(现在不记得它叫什么了),然后创建一个进度对话框,它将后台工作人员传递给该对话框。然后它以模态方式显示进度对话框,这意味着它将等待(但会发送消息)直到进度对话框关闭。

        进度表单负责从其 OnLoad 覆盖启动 BackgroundWorker,并在看到 BackgroundWorker 完成时自行关闭。显然,您可以在进度表单中添加消息文本、进度条、取消按钮等。

        【讨论】:

        • 我不认为我想要 DoEvents() 正是您列出的原因。您能否详细说明带有模态进度对话框的后台工作人员?那么,我可以弹出一个模态表单,从 bg 线程更新它,甚至让 bg 线程杀死它?
        • @Will。来这里是为了准确地发布您的对话内容......我认为这是最好的解决方案。
        【解决方案4】:

        构建您的应用程序,以便主线程仅执行 UI 更新,而所有其他工作都通过工作队列在辅助线程上完成;然后在你的主线程中添加一个 waiting-for-godot 标志,并使用它来保护将项目添加到工作队列的方法

        出于好奇:你为什么要这样做?

        【讨论】:

        • 就您所建议的更改应用程序结构而言,这不完全是我的应用程序;那艘船已经航行了。
        • 至于我为什么要这样做,我的任务是移动应用程序的一个子系统以在后台线程中工作,但仍然阻止用户的工作流程(主线程) 仅适用于某些类型的工作。
        • @CrashCodes:太模糊了,抱歉; “有时”和“某些类型”可能意味着任何东西——你将如何控制时间和内容?
        • “有时”和“某些类型”不相关。它们将由简单的 if 语句控制,以确定是否等待或等待多长时间。关键是我需要更新 UI,同时阻止用户通过应用程序的进度。
        • @CrashCodes: 如果这就是你想要的,请在等待期间定期调用 Application.DoEvents()
        【解决方案5】:

        您可能应该按照其他人的建议重构您的代码,但根据您正在寻找的行为,您可能还想看看在后台工作线程上使用 Thread.Join。 Join 实际上允许调用线程在等待其他线程完成时处理 COM 和 SendMessage 事件。这似乎在某些情况下可能很危险,但我实际上有几个场景,这是等待另一个线程干净完成的唯一方法。

        Thread..::.Join 方法

        阻塞调用线程直到 线程终止,同时继续 执行标准 COM 和 SendMessage 抽水。

        (来自http://msdn.microsoft.com/en-us/library/95hbf2ta.aspx

        【讨论】:

          【解决方案6】:

          我同意其他建议您使用后台工作者的观点。它完成了繁重的工作并允许 UI 继续。您可以使用后台工作人员的报告进度来启动可以将主窗体设置为在后台执行操作时禁用的时间,然后在“某些实例”完成处理后重新启用。

          如果这有帮助,请告诉我! 捷豹路虎

          【讨论】:

            【解决方案7】:

            如果您可以调整您的代码,以便在流程开始后设置一个标志,然后在开始其他操作之前在 UI 中检查该标志,我认为您可以更轻松地编写此代码。我将创建一个委托,该委托可以从线程池中的线程或用户创建的线程中调用,以更新 UI 中的进度。后台进程完成后,切换标志,现在可以继续正常的 UI 操作。您需要注意的唯一警告是,当您更新 UI 组件时,您必须在创建它们的线程(主/UI 线程)上执行此操作。为了实现这一点,您可以在该线程上的任何控件上调用 Invoke() 方法,并将调用它所需的委托和参数传递给它。

            这是我前段时间写的关于如何使用 Control.Invoke() 的教程的链接:

            http://xsdev.net/tutorials/pop3fetcher/

            【讨论】:

              【解决方案8】:

              只是一个代码sn-p:没有太多时间抱歉:)

                  private void StartMyDoSomethingThread() {
                      Thread d = new Thread(new ThreadStart(DoSomething));
                      d.Start();
                  }
              
                  private void DoSomething() {
                      Thread.Sleep(1000);
                      ReportBack("I'm still working");
                      Thread.Sleep(1000);
                      ReportBack("I'm done");
                  }
              
                  private void ReportBack(string p) {
                      if (this.InvokeRequired) {
                          this.Invoke(new Action<string>(ReportBack), new object[] { p });
                          return;
                      }
                      this.Text = p;
                  }
              

              【讨论】:

                【解决方案9】:

                最好分派工作,但如果必须,也许是这样的。只需调用此方法等待信号,而不是调用waitone。

                private static TimeSpan InfiniteTimeout = TimeSpan.FromMilliseconds(-1); 
                private const Int32 MAX_WAIT = 100; 
                
                public static bool Wait(WaitHandle handle, TimeSpan timeout) 
                { 
                    Int32 expireTicks; 
                    bool signaled; 
                    Int32 waitTime; 
                    bool exitLoop; 
                
                    // guard the inputs 
                    if (handle == null) { 
                        throw new ArgumentNullException("handle"); 
                    } 
                    else if ((handle.SafeWaitHandle.IsClosed)) { 
                        throw new ArgumentException("closed wait handle", "handle"); 
                    } 
                    else if ((handle.SafeWaitHandle.IsInvalid)) { 
                        throw new ArgumentException("invalid wait handle", "handle"); 
                    } 
                    else if ((timeout < InfiniteTimeout)) { 
                        throw new ArgumentException("invalid timeout <-1", "timeout"); 
                    } 
                
                    // wait for the signal 
                    expireTicks = (int)Environment.TickCount + timeout.TotalMilliseconds; 
                    do { 
                        if (timeout.Equals(InfiniteTimeout)) { 
                            waitTime = MAX_WAIT; 
                        } 
                        else { 
                            waitTime = (expireTicks - Environment.TickCount); 
                            if (waitTime <= 0) { 
                                exitLoop = true; 
                                waitTime = 0; 
                            } 
                            else if (waitTime > MAX_WAIT) { 
                                waitTime = MAX_WAIT; 
                            } 
                        } 
                
                        if ((handle.SafeWaitHandle.IsClosed)) { 
                            exitLoop = true; 
                        } 
                        else if (handle.WaitOne(waitTime, false)) { 
                            exitLoop = true; 
                            signaled = true; 
                        } 
                        else { 
                            if (Application.MessageLoop) { 
                                Application.DoEvents(); 
                            } 
                            else { 
                                Thread.Sleep(1); 
                            } 
                        } 
                    } 
                    while (!exitLoop); 
                
                    return signaled;
                }
                

                【讨论】:

                • CF 不支持Application.MessageLoop
                • DoEvents 将允许用户按下按钮等。我只是希望屏幕能够更新。
                【解决方案10】:

                我选择了一些我还没有看到的东西,那就是使用 MessageQueues。

                • MainThread 在等待队列中的下一条消息时阻塞。
                • 后台线程将不同类型的消息发布到 MessageQueue。
                • 某些消息类型向 MainThread 发出信号以更新 UI 元素。
                • 当然有消息告诉MainThread停止阻塞和等待消息。

                考虑到 Windows 消息循环已经存在于某处,这似乎太过分了,但它确实有效。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2010-12-16
                  • 1970-01-01
                  • 2014-05-20
                  • 1970-01-01
                  • 2016-05-24
                  • 1970-01-01
                  • 1970-01-01
                  • 2020-04-26
                  相关资源
                  最近更新 更多