一个线程问题:在Mono中会卡住而在MS.Net中不会。

7

我正在使用Mono测试我的应用程序,以便进行Linux端口,但我遇到了一个线程问题。最初我考虑在此处粘贴3000行代码,但最终我设计了一个小的最小示例;)

您有一个带有按钮(诗意地命名为Button1)和标签(不出所料,名称为Label1)的窗体。整个窗体位于名为Form1的表单上,并且单击Button1会启动一个无限循环,该循环会递增本地计数器并更新Label1(使用Invoke)以反映其值。

现在,在Mono中,如果调整窗体大小,则标签将停止更新,永远不会重新启动。这在MS实现中不会发生。 BeginInvoke也不起作用;更糟糕的是,它会使UI在两种情况下挂起。

您知道这种差异来自哪里吗?你会如何解决它?最后,为什么这里BeginInvoke不起作用?我一定犯了一个很大的错误...但是是什么呢?


编辑:: 目前为止有一些进展:

  • 实际上,调用BeginInvoke确实起作用;只是UI刷新得不够快,因此似乎会停止。
  • 在mono中,当您向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) {
        }
    }
}
3个回答

5

这是mono运行时中的一个bug,至少我认为是这样。代码可能不是很规范(我不是一个多线程专家),但表明bug的事实是行为在Windows和Linux上不同。

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

在Windows上,mono显示了所有前面提到的问题。我已经在https://bugzilla.novell.com/show_bug.cgi?id=690400发布了一个错误报告。


太好了。随意接受您自己的答案。在SO上是允许的,甚至是鼓励的! - Brian Gideon
好的 =)谢谢你的回答,我一定会遵循你的建议 :) (我在评论中留了一个小问题) - Clément
我建议使用成熟的GUI工具包,比如GTK或Qt。WinForms工具包(不带Mono)甚至没有实现Ctrl+BackSpace。尽管我自己在办公室有一个用WinForms编写的应用程序,但我承认这是一个错误。 - Hi-Angel

1
你知道这个差异来自哪里吗?你会如何解决它?
我不确定。在你的代码中,我没有看到任何明显的问题会导致Mono和.NET之间的差异。如果我必须猜测,我会说你可能遇到了Mono中一个晦涩的错误。虽然,我想这也有可能是因为Mono使用了足够不同的机制来处理导致窗体刷新的WM_PAINT消息。重复调用Invoke对UI线程的不断轰击可能会干扰Mono刷新窗体的能力。
最后,为什么BeginInvoke在这里不起作用?
在紧密循环中调用Invoke已经够糟糕了,但是BeginInvoke会更糟。工作线程正在淹没UI消息泵。BeginInvoke不会等待UI线程完成执行委托。它只是发布请求并快速返回。这就是为什么它似乎挂起了。BeginInvoke正在向UI消息队列发布的消息随着工作线程严重超过UI线程处理它们的能力而不断增加。
其他评论

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

解决方案

一种可能的解决方法是放慢调用Invoke的速度。尝试在每1000次迭代或类似情况下进行调用。

更好的方法是根本不使用InvokeBeginInvoke。个人认为这些更新UI的机制被过度使用。让UI线程自行限制其更新速率通常更好,特别是当工作线程正在进行连续处理时。这意味着您需要在窗体上放置一个计时器,并将其设置为所需的刷新速率。从Tick事件中,您将探测一个共享数据结构,该工作线程正在更新,并使用该信息来更新表单上的控件。这有几个优点。

  • 它打破了Control.Invoke所强加的UI和工作线程之间的紧密耦合。
  • 它将更新UI线程的责任放在了本应属于UI线程的地方。
  • UI线程可以决定何时以及多久进行更新。
  • 与由工作线程发起的调度技术相比,没有UI消息泵被超载的风险。
  • 工作线程不必等待确认更新已执行后再继续下一步操作(即在UI和工作线程上获得更高的吞吐量)。

非常感谢您详尽的回答和建议,我非常感激 =) 经过大量调查,我认为我找到了问题所在。我会将其发布为单独的答案。 - Clément

0

首先:点击Button1已经是异步的了,所以你不需要创建另一个线程来增加,只需调用增加方法抱歉,我一边逐行阅读你的问题,一边到while循环时忘记了按钮:

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

您还可以“自动化”Invoke Required“模式”:自动化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);
        }
    }
}

1
当你说点击按钮已经是异步的时候,你的意思是什么?在你的建议中,对Button1_Click的调用将永远不会退出。 - Clément
我放弃了关于后台/前台问题的疑问,专注于当窗体被调整大小时线程停止运行的问题。你的解决方案根本不起作用;调用Increment()从未退出,界面也没有更新。 - Clément
这里检查 InvokeRequired 不是无用的吗?你的示例永远不需要调用,因为它直接在 UI 线程上运行,而我的示例则始终需要一个。 - Clément
使用您的代码,在单独的线程上调用Increment确实可以解决在mono上的问题。这仍然让我想知道为什么同步调用会在mono上挂起。 - Clément
抱歉,我正在逐行回答您的问题,当我到达while循环时,我忘记了按钮。你是对的,你确实需要线程,但问题的其余部分是有效的。 - Kiril
显示剩余2条评论

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