在C#中,如何在等待主线程的同时继续处理UI更新?(.NET 2.0 CF)

3

我希望能够在不影响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()调用时应用程序将会死锁。


至于为什么我想这样做,我被要求将一个应用程序的一个子系统移动到后台线程中进行工作,但仍然只有在某些情况下和与该子系统相关的特定工作时才阻止用户的工作流程(主线程)。

10个回答

2
你想要使用 "BackgroundWorker" 类,它可以为你减轻大部分的痛苦。但正如之前提到的,你还需要将其结构化,以便主线程更新 UI,而工作线程则负责完成繁重的任务。

2
常见阻塞主线程的操作包括打开消息框或模态对话框,主代码将在MessageBox或ShowDialog调用时阻塞。这些项的工作方式(而MessageBox只是一种专门的模态对话框)是它们在阻塞时包含自己的消息泵。
虽然这是一种恶劣的hack,但您可以像这样在应用程序中循环调用Application.DoEvents(),以便在等待其他任务完成时保持用户消息在泵送。您需要小心,因为这样泵送消息可能会导致各种糟糕的事情发生 - 例如,有人关闭了窗体或重新进入当前的消息处理程序 - 模态对话框通过有效地禁用从启动它们的窗体输入来避免这种情况。
如果您能够让其适合,则BackgroundWorker是一个更好的解决方案。我有时将其与模态“进度对话框”结合使用,以便给我背景线程/消息泵送和阻止UI线程的机会。
编辑 - 关于最后一部分的扩展:
我使用过的一种方法是创建一个“进度表单”类,该类将BackgroundWorker对象作为构造函数参数,并包含传递给它的后台工作者的进度和完成事件的处理程序。
想要完成工作的窗体创建后台工作者并连接“工作”事件(现在无法记住它的名称),然后创建一个进度对话框,将其传递给后台工作者。然后,它模态显示进度对话框,这意味着它会等待(但在泵送消息)直到进度对话框关闭。
进度表单负责从其OnLoad覆盖中启动BackgroundWorker,并在看到BackgroundWorker完成时关闭自己。显然,您可以向进度表单添加消息文本、进度条、取消按钮等。

我认为我不想使用DoEvents(),正是你列出的原因。你能详细说明一下带有模态进度对话框的后台工作器吗?这样,我可以弹出一个模态窗体,从后台线程更新它,甚至让后台线程关闭它? - CrashCodes
@Will。来这里就是为了发布你的对话所做的事情... 我认为这是最好的解决方案。 - Nicholas Mancuso

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方法),甚至整个窗体!!

1

将你的应用程序结构化,使得主线程只执行UI更新,而所有其他工作都通过一个工作队列在辅助线程上完成;然后向主线程添加一个等待flag,并使用它来保护向工作队列添加项目的方法。

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


就像你建议的那样更改应用程序结构,这并不完全是我的应用程序;而且已经来不及了。 - CrashCodes
关于为什么我想这样做,是因为我被分配任务将应用程序的一个子系统移动到后台线程中进行工作,但仍然需要它有时阻止用户的工作流程(主线程),并且只针对某些类型的工作。 - CrashCodes
@CrashCodes:太模糊了,抱歉;“有时候”和“某些类型”可以意味着任何事情 - 你打算如何控制何时以及什么? - Steven A. Lowe
“有时”和“某些类型”并不相关。它们将由简单的if语句控制,以确定是否等待或等待多长时间。关键是我需要在阻止用户通过应用程序的同时更新UI界面。 - CrashCodes
@CrashCodes:如果你只是想要这个,那么在等待时定期调用Application.DoEvents()即可。 - Steven A. Lowe

0
你应该按照其他人的建议重新组织你的代码,但是根据你想要的行为,你可能还希望查看在后台工作线程上使用 Thread.Join 的方法。Join 实际上允许调用线程在等待另一个线程完成时处理 COM 和 SendMessage 事件。这似乎在某些情况下可能很危险,但我实际上有过几种情况,它是等待另一个线程干净地完成的唯一方法。

Thread..::.Join 方法

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

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

0

我同意其他人建议您使用后台工作程序。它可以完成繁重的工作并允许UI继续运行。您可以使用Background Worker的Report Progress来启动时间,在此期间主窗体可以被设置为禁用,而在后台执行操作完成后再重新启用。

如果这有帮助,请告诉我! JFV


0
如果您能调整代码,使其在进程开始时设置标志,然后在启动其他操作之前在UI中检查该标志,我认为编写代码会更容易。我会创建一个委托,可以从线程池或用户创建的线程中调用以更新UI上的进度。一旦后台进程完成,切换标志,现在正常的UI操作可以继续。您需要注意的唯一注意事项是,当您更新UI组件时,必须在它们被创建的线程上执行,即主/UI线程。为了实现这一点,您可以在任何控件上调用Invoke()方法,并传递您需要调用它的委托和参数。
这是我一段时间前撰写的关于如何使用Control.Invoke()的教程链接:

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


0

我选择了一种我还没有看到过的方法,那就是使用消息队列。

  • 主线程在等待队列中的下一条消息时会阻塞。
  • 后台线程会向消息队列中发布不同类型的消息。
  • 其中一些消息类型会通知主线程更新UI元素。
  • 当然,还有一条消息告诉主线程停止阻塞并等待消息。

考虑到Windows消息循环已经存在于某个地方,这似乎有点过头了,但它确实有效。


0

只是一小段代码:抱歉没有太多时间 :)

    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;
    }

0
最好是派遣工作,但如果必须的话,可以尝试这样做。只需调用此方法等待信号,而不是调用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。 - CrashCodes
DoEvents会允许用户按按钮等操作,但我只想更新屏幕。 - CrashCodes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接