【问题标题】:Waiting for a long process and still updating UI等待漫长的过程,仍在更新UI
【发布时间】:2024-07-20 15:50:02
【问题描述】:

我一直在尝试创建一个在不阻塞 UI 线程的情况下写入数据库的任务。我遇到的最大问题是等待该进程完成而不会发生阻塞。

我一直在努力避免使用DoEvents(虽然现在通过这个程序非常频繁地使用它,但我想在继续前进的同时停止使用它)。

我尝试创建进程以在第二个线程上运行并等待它完成以及使用BackgroundWorker

我遇到的问题不是让代码在不同的线程中运行,而是试图找到一种方法来等待它完成。

基本上,现在我执行以下操作:

  1. 连接到数据库
  2. 创建一个后台工作者(或线程)来写入数据库(我可能最终会使用BackgroundWorker,所以我可以使用ReportProgress
  3. 启动线程或BackgroundWorker
  4. 使用 While 循环等待线程 /BackgroundWorker 完成。对于线程,我等待 IsAlive 变为 false,对于 BackgroundWorker,我切换一个布尔变量。
  5. 我让用户知道该过程已完成。

问题出在#4。

执行没有代码的 while 循环,或者 Thread.Sleep(0) 使 UI 被阻塞(Thread.Sleep(0) 使程序也占用 100% 的程序资源)

所以我这样做:

while (!thread.IsAlive)
   Thread.Sleep(1);

-或-

while (bProcessIsRunning)
   Thread.Sleep(1);

这会阻止 UI。

如果我在那里调用Application.DoEvents(),UI 会更新(虽然它是可点击的,所以我必须在此过程运行时禁用整个表单)。

如果我同步运行该过程,我仍然需要创建某种方式来更新 UI(在我看来,是一个 DoEvents 调用),因此它看起来不会被锁定。

我做错了什么?

【问题讨论】:

  • 欢迎来到Stack Overflow。几件小事: 1. 请不要在标题前加上“C# .NET (3.0)”之类的东西。这就是Stack Overflow 上使用的标签。 2. 请不要签名。 3.说到标签,你应该通过添加适当的标签告诉我们你是使用winforms还是wpf或其他什么。
  • 抱歉,我不知道用什么来标记它,并且用 5 标记上限,我没有放它。我正在使用 Winforms(现在是一个标签)
  • @JohnSaunders:我强烈不同意您在标题中不包含标签(例如 C# .NET WinForms)的建议。原因:Google 是人们访问 * 页面的最常见方式。拥有包含此类上下文的标题可以更快地确定问答是否相关。
  • @ToolmakerSteve:你错了。 Stack Overflow 已经将顶部标签放入标题中,如 Google 所见。
  • @JohnSaunders:如果标签直接列在标题下方,那么 POV 将是明智的。但他们不是。这不是“噪音”!一个人必须扫描到一个可能很长的问题的结尾才能找出问题的关键背景,知道一个人是否关心这个问题,这种想法完全是错误的。

标签: c# .net winforms multithreading doevents


【解决方案1】:

首先,你为什么要避开DoEvents()

其次,您使用了相互矛盾的术语。

等待 == 阻塞

您说您想要阻塞 UI 线程,但您确实想要等待任务完成。这些是相互排斥的状态。如果您等待某事完成,则说明您正在阻塞线程。

如果您希望 UI 真正可用(不被阻止),那么您不必等待您的任务完成。只需注册一个事件处理程序以在完成时触发。例如,使用 BackgroundWorker,处理 RunWorkerCompleted 事件。对于任务,您可以使用 continuation 将回调分派到您的主线程。

但您似乎只想更新 UI,而不是可用。通常,只有在您希望进度条或其他一些 UI 动画保持移动时才有意义。在这种情况下,我会打开一个模式对话框,启动我的任务,然后等待它,是的,调用 DoEvents()。

    var dialog = new MessageBoxFormWithNoButtons("Please wait while I flip the jiggamawizzer");
    dialog.Shown += (_, __) =>
        {
            var task = Task.Factory.StartNew(() => WriteToDatabase(), TaskCreationOptions.LongRunning);
            while (!task.Wait(50))  // wait for 50 milliseconds (make shorter for smoother UI animation)
                Application.DoEvents(); // allow UI to look alive
            dialog.Close();
        }

    dialog.ShowDialog();

模态对话框阻止用户做任何事情,但任何动画仍然可以工作,因为 DoEvents() 每秒被调用 20 次(或更多)。

(您可能希望为不同的任务完成状态添加特殊处理,但这是题外话。)

【讨论】:

    【解决方案2】:

    C# 使用事件模型——您要做的是分派完成工作的进程,然后让该进程在完成时触发自定义事件或使用其中一个线程事件。当进程在“后台”运行时,将控制权从您的代码中释放回系统。

    【讨论】:

    • 这是否意味着我应该启动 BackgroundWorker,然后让所有代码执行直到 RunWorkerCompleted 触发?这是否意味着用户仍然可以在进程运行时与表单交互(与 DoEvents 相同)?它还增加了对全局变量(例如数据库连接)的需求,并且如果调用函数在一个类中,而不是在表单本身中,则需要更多的代码。
    • 有很多方法可以解决这个问题。简单的是禁用界面并放置一个微调器或其他一些视觉效果。然后在异步触发时将其拆除。其他选项是仅在后台运行时“冻结”控件或以不同方式处理响应。
    • 我不知道为什么这需要更多的代码,应该是闭包应该允许你在你的线程中使用局部变量。你想看看这样的例子吗?
    • 我在我的班级中创建了使用 BackgroundWorker 委托的事件。这样我就可以直接传递信息而无需任何处理。在此过程中,我将为表单创建一个进度条或微调器。谢谢!
    • @TrevorWatson 没错。完美的。祝你好运。
    【解决方案3】:

    您不能在 UI 线程上等待。

    相反,将处理程序添加到Exited event

    【讨论】:

      【解决方案4】:

      我不知道这是否会简化您的问题,但我们使用基本对象控件:http://www.essentialobjects.com/Products/EOWeb/ 来管理我们漫长的流程。

      【讨论】:

      • 很遗憾,我们目前无法购买任何其他组件。不过,我确实喜欢他们的一些 Web 界面组件。
      • 进度组件是免费的。
      【解决方案5】:

      如果您将长 db 操作委托给后台工作线程,那么 通过从工作人员触发 ProgressChangedEvent 来发出进度信号,您可以处理更新 UI 中的进度条。 通过触发 RunWorkerCompleteEvent 发出同样完成的信号。

      无需轮询/循环执行所需的事件。

      问题是当你的后台线程正在做一些你不允许在表单中做的事情时。

      关闭它,改变一个编辑框?等等。 这是通过某种状态机完成的,就像您在启动线程时禁用按钮然后在 RunWorkerCompleted 事件中再次启用它一样简单。 你们可以不理会 buutom 并在其 Click 处理程序中检查一个名为 busy 的布尔值。

      您是为了显示进度而卸载进程,还是只是为了避免“Windows 没有响应”,或者用户在进程中是否可以明智地做其他事情,例如关闭表单、取消操作等。

      一旦您弄清楚 UI 应该做什么,您就可以将 backgrondworker 事件挂钩到一些将管理事物的代码中。

      【讨论】:

      • 这主要是为了“窗口没有响应”消息,但在某些时候应该有一些通知给用户一个进程正在运行,所以他们不认为我们已经崩溃了。