线程安全的事件 - 这是一种“干净”的方式吗?

4

我在专业库的一些代码中遇到了一些问题,不确定这是否是处理跨线程事件调用的有效方式。

下面的代码位于一个窗体应用程序中。线程调用是从一个类中进行的,该类本身启动一个新线程并接收消息:

private void Library_StatusChanged(object sender, AbstractTestCase.StatusChangedEventArgs e)
{
    if (this.InvokeRequired)
    {
        this.lblProgress.Invoke((MethodInvoker)delegate ()
        {
            lblProgress.Text = "Current state: " + e.Step;
            lblProgress.Refresh();
        }
        );

        this.pbProgess.Invoke((MethodInvoker)delegate ()
        {
            pbProgess.Value = e.Percentage;
            pbProgess.Refresh();
        });

        this.lstStatus.Invoke((MethodInvoker)delegate ()
        {
            lstStatus.Items.Add("    " + e.Step);
            lstStatus.Refresh();

        });

        this.Invoke((MethodInvoker)delegate ()
        {
            this.Refresh();
        });
    }
    else
    {
        lblProgress.Text = "Current state:" + e.Step;
        lblProgress.Refresh();

        pbProgess.Value = e.Percentage;
        pbProgess.Refresh();

        lstStatus.Items.Add("    " + e.Step);
        lstStatus.Refresh();

        this.Refresh();
    }

    Application.DoEvents();
}

这算是“尖端技术”吗?在我看来有点凌乱啊?!


我通常认为使用 Application.DoEvents 是某些问题的信号。而且只使用一个 Invoke 可能会更有效率,而不是分别使用 4 个。 - Matt Burland
该库是单线程的(除了一些罕见的多线程部分),因此用户界面会冻结。 - AllDayPiano
1
"and thus the UI freezes" 及 Application.DoEvents 是一种非常糟糕的方法来掩盖这个问题。 - Matt Burland
这不是我的主意 :) - AllDayPiano
2
记住DoEvents的作用。它不能神奇地将堆栈解开回到线程的消息循环。它处理另一个消息。假设该消息导致库状态更改?我们现在正在*递归调用事件处理程序。如果这种情况不断发生,什么会阻止无限递归?我在这里看不到任何代码。这段代码显然没有使用最佳实践,并且可能是由不理解消息处理的人编写的。 - Eric Lippert
显示剩余2条评论
2个回答

5

现在的技术水平使用await。如果这里不可行,至少将代码简化为单个Invoke调用。不需要在每个控件上调用,只需要在UI线程上任何地方调用即可。

InvokeRequired检查不应该是必需的,因为你应该知道事件在哪个线程上引发。

无论如何复制逻辑,例如"Current state: " + e.Step都是一个非常糟糕的想法。无论如何我都会在代码审查中失败。

存在Application.DoEvents是一个非常糟糕的迹象。可能是一种误解,因为它只有在UI线程上调用才有意义,但是既然已经在UI线程上了,为什么还要Invoke?看起来像是矛盾的。

lstStatus.Refresh();也是一种误解,可能是迷信。控件会自动刷新(如果允许事件处理)。


嗨usr,回答你的问题:调用是必需的,因为事件可能不是由同一线程触发的。有类似于在另一个线程中运行的异步串口访问的东西。也可能发生UI线程完全忙碌,因此DoEvent可能会重新绘制表单?!Control.Refresh()同理。 - AllDayPiano
1
lstStatus.Refresh(); 这也是一个误解,可能是迷信的。 货物崇拜式编程。 - Matt Burland
@MattBurland 嗯,那个人曾经这样做过,并隐藏了用户界面线程的冻结,所以他永远认为这是解决方案。 - usr
@AllDayPiano 好的,所以我们 总是 需要调用。删除 InvokeRequired 检查。; "它也可能发生,UI 线程完全忙碌" 是不好的设计。我不会严格称其为 bug,但是不希望出现这种情况。; 在后台线程上运行 DoEvents 不起作用,必须在 UI 线程上运行。给 DoEvents 调用一个 Refresh 是 100% 多余的。 - usr
所以这是一个结构性问题,对吧?但是 - 然而 - 当 CPU 负载超过 100% 时,如何重新绘制表单呢?就我在编程方面的经验而言,我会启动另一个线程并异步运行 UI。 - AllDayPiano
显示剩余3条评论

1
当你使用invoke时,会将一个语句添加到队列中,由UI线程进行处理。
使用这个简单的解决方案:
private void Library_StatusChanged(object sender, AbstractTestCase.StatusChangedEventArgs e)
{
    this.lblProgress.Invoke((MethodInvoker)delegate ()
    {
        lblProgress.Text = "Current state: " + e.Step;
    });

    this.pbProgess.Invoke((MethodInvoker)delegate ()
    {
        pbProgess.Value = e.Percentage;
    });

    this.lstStatus.Invoke((MethodInvoker)delegate ()
    {
        lstStatus.Items.Add("    " + e.Step);
    });
}

1
这不是原帖作者的代码,而是来自一个库。我假设他们没有计划重新编写该库。他们只是在询问他们遇到的代码问题。 - Matt Burland
对,马特,你说得对。我不太喜欢这个库,但免费的东西就是便宜,便宜的东西就是好的,好的东西就接近完美,而完美就是期望的。 :-/ - AllDayPiano
@AllDayPiano:我必须不同意。正如这段代码清楚地表明的那样,“便宜”并不意味着“好”。 - Stephen Cleary
哈哈,Stephen,你说得对。但不要告诉我 - 告诉我的上级吧 :) - AllDayPiano

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