7

我正在使用单声道测试我的应用程序以预置 Linux 端口,但我遇到了线程问题。我最初考虑在这里粘贴 3000 行代码,但最后我设计了一个小的最小示例;)

您有一个带有按钮的表单(诗意地命名为Button1,和一个标签(毫无疑问,它带有名称Label1))。整个过程就是在一种叫做Form1. 单击Button1会启动一个无限循环,该循环会增加本地计数器并更新Label1(使用Invoke)以反映其值。

现在在 Mono 中,如果您调整表单大小,标签将停止更新,永远不会重新启动。MS 实现不会发生这种情况。BeginInvoke没有更好的工作;更糟糕的是,它会使 UI 在这两种情况下都挂起。

你知道这种差异是从哪里来的吗?你会怎么解决?最后,为什么 BeginInvoke 在这里不起作用?我一定是犯了一个大错……但是哪个?


编辑:到目前为止的一些进展:

  • 调用 BeginInvoke 确实有效;只是,用户界面的刷新速度不够快,所以它似乎停止了。
  • 在单声道上,当您在 UI 队列中插入消息(例如,通过调整表单大小)时,整个线程会挂起。事实上,同步Invoke调用永远不会返回。我试图理解为什么。
  • 有趣的是:即使使用BeginInvoke,在调整大小操作结束之前也不会执行异步调用。在 MS.Net 上,它们在调整大小的同时继续运行。

代码如下所示(C# 版本较低):

Public Class Form1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim T As New Threading.Thread(AddressOf Increment)
        T.Start()
    End Sub

    Sub UpdateLabel(ByVal Text As String)
        Label1.Text = Text
    End Sub

    Delegate Sub UpdateLabelHandler(ByVal Text As String)
    Sub Increment()
        Dim i As Long = 0
        Dim UpdateLabelDelegate As New UpdateLabelHandler(AddressOf UpdateLabel)
        Try
            While True
                i = (i + 1) Mod (Long.MaxValue - 1)
                Me.Invoke(UpdateLabelDelegate, New Object() {i.ToString})
            End While
        Catch Ex As ObjectDisposedException
        End Try
    End Sub
End Class

或者,在 C# 中,

public class Form1
{
    private void Button1_Click(System.Object sender, System.EventArgs e)
    {
        System.Threading.Thread T = new System.Threading.Thread(Increment);
        T.Start();
    }

    public void UpdateLabel(string Text)
    {
        Label1.Text = Text;
    }

    public delegate void UpdateLabelHandler(string Text);
    public void Increment()
    {
        long i = 0;
        UpdateLabelHandler UpdateLabelDelegate = new UpdateLabelHandler(UpdateLabel);
        try {
            while (true) {
                i = (i + 1) % (long.MaxValue - 1);
                this.Invoke(UpdateLabelDelegate, new object[] { i.ToString() });
            }
        } catch (ObjectDisposedException Ex) {
        }
    }
}
4

3 回答 3

5

这是单声道运行时中的一个错误,至少我认为是。该代码可能不是一个好的实践(我不是线程专家),但表明存在错误的事实是 Windows 和 Linux 上的行为不同。

在 Linux 上,mono 的行为与 MS.Net 在 Windows 上的行为完全相同。即使在调整大小时也不会挂起、持续更新。

在 Windows 上,单声道显示所有上述问题。我在https://bugzilla.novell.com/show_bug.cgi?id=690400发布了错误报告。

于 2011-04-27T21:38:48.163 回答
1

你知道这种差异是从哪里来的吗?你会怎么解决?

我不知道。我在您的代码中看不到任何明显会导致 Mono 和 .NET 之间差异的内容。如果我不得不做出一个疯狂的猜测,我会说你有可能偶然发现了 Mono 中一个不起眼的错误。不过,我认为 Mono 可能使用完全不同的机制来处理导致表单刷新的 WM_PAINT 消息。UI 线程因重复调用而不断受到冲击,这Invoke可能会破坏 Mono 刷新表单的能力。

最后,为什么 BeginInvoke 在这里不起作用?

在一个紧密Invoke的循环中跟注已经够糟糕了,但BeginInvoke会更糟。工作线程正在淹没 UI 消息泵。BeginInvoke不会等到 UI 线程完成执行委托。它只是发布请求并快速返回。这就是它似乎挂起的原因。发布到 UI 消息队列的消息BeginInvoke不断增加,因为工作线程可能严重超出 UI 线程处理它们的能力。

其他的建议

我还应该提到,工作线程在代码中几乎没用。原因是您Invoke在每次迭代时都会调用。Invoke阻塞直到 UI 完成代理的执行。这意味着您的工作线程和 UI 线程基本上彼此同步。换句话说,worker 大部分时间都在等待 UI,反之亦然。

解决方案

一种可能的解决方法是减慢Invoke调用速度。不要在每次循环迭代时调用它,而是尝试每 1000 次迭代等执行一次。

任何更好的方法是不使用Invoke或根本不使用BeginInvoke。就个人而言,我认为这些更新 UI 的机制被过度使用了。让 UI 线程控制自己的更新速率几乎总是更好,尤其是在工作线程进行连续处理时。这意味着您需要在表单上放置一个计时器,并让它以所需的刷新率打勾。从Tick事件中,您将探测工作线程正在更新的共享数据结构,并使用该信息来更新表单上的控件。这有几个优点。

  • 它打破了 UI 和工作线程之间的紧密耦合Control.Invoke
  • 它将更新 UI 线程的责任放在它应该属于的 UI 线程上。
  • UI 线程可以决定更新的时间和频率。
  • 不存在 UI 消息泵溢出的风险,就像工作线程启动的编组技术那样。
  • 工作线程不必等待确认更新已执行,然后再继续执行后续步骤(即,您可以在 UI 和工作线程上获得更多吞吐量)。
于 2011-04-27T17:17:32.270 回答
0

首先也是最重要 的:点击 Button1 已经是异步的,所以你不需要创建另一个线程来递增,只需调用递增方法对不起,我正在逐行阅读你的问题,当我进入 while-loop 时我忘记了按钮:

private void Button1_Click(System.Object sender, System.EventArgs e)
{
    Thread t = new Thread(Increment);
    t.IsBackground = true;
    t.Start();
}

第二:如果您确实需要使用线程,那么您应该始终将线程设置为后台(即前台阻止您的进程终止),除非您有充分的理由使用前台线程。

第三:如果您正在对 UI 进行更新,那么您应该检查InvokeRequired属性并调用BeginInvoke

public void UpdateLabel(string Text)
{

    if (InvokeRequired)
    {
        BeginInvoke(new UpdateLabelDelegate(UpdateLabel), Text);
    }
    else
    {
        Label1.Text = Text;
    }
}

public void Increment()
{
    int i = 0;
    while(true)
    {
        i++; // just incrementing i??
        UpdateLabel(i.ToString());

        Thread.Sleep(1000);// slow down a bit so you can see the updates
    }
}

您还可以“自动化”调用所需的“模式”:自动化 InvokeRequired 代码模式

现在看看你是否还有同样的问题。

我在我的机器上试过它,它就像一个魅力:

public partial class Form1 : Form
{
    private delegate void UpdateLabelDelegate(string text);
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Thread t = new Thread(Increment);
        t.IsBackground = true;
        t.Start();
    }

    private void UpdateLabel(string text)
    {
        if (label1.InvokeRequired)
        {
            BeginInvoke(new UpdateLabelDelegate(UpdateLabel), text);
        }
        else
        {
            label1.Text = text;
        }

    }

    private void Increment()
    {
        int i = 0;
        while (true)
        {
            i++;
            UpdateLabel(i.ToString());
            Thread.Sleep(1000);
        }
    }
}
于 2011-04-27T13:49:20.060 回答