内部控件上的“跨线程操作无效”异常

7

我已经苦苦挣扎了相当长一段时间: 我有一个函数,旨在添加控件到具有跨线程处理功能的面板上,问题是尽管面板和控件都处于“InvokeRequired=false”的状态,但我仍然会收到异常提示,告诉我其中一个控件的内部控件是从创建它的线程不同的线程中访问的,代码片段如下:

public delegate void AddControlToPanelDlgt(Panel panel, Control ctrl);
    public void AddControlToPanel(Panel panel, Control ctrl)
    {
        if (panel.InvokeRequired)
        {
            panel.Invoke(new AddControlToPanelDlgt(AddControlToPanel),panel,ctrl);
            return;
        }
        if (ctrl.InvokeRequired)
        {
            ctrl.Invoke(new AddControlToPanelDlgt(AddControlToPanel),panel,ctrl);
            return;
        }
        panel.Controls.Add(ctrl); //<-- here is where the exception is raised
    }

异常信息如下:
"跨线程操作无效:从创建控件的线程以外的线程访问控件 'pnlFoo'"
('pnlFoo' 在 ctrl.Controls 下)
我怎么把ctrl添加到panel中?!
当代码执行 "panel.Controls.Add(ctrl);" 这行时 - panel 和 ctrl 的 "InvokeRequired" 属性都为 false,问题在于 ctrl 中的控件的 "InvokeRequired" 设置为 true。澄清一下:“panel” 在基本线程上创建,“ctrl” 在新线程上创建,所以“panel”必须被调用(导致“ctrl”需要再次调用)。当这两个调用都完成后,它会到达 panel.Controls.Add(ctrl) 命令(此状态下“panel”和“ctrl”都不需要调用)。
以下是程序的小片段:
public class ucFoo : UserControl
{
    private Panel pnlFoo = new Panel();

    public ucFoo()
    {
        this.Controls.Add(pnlFoo);
    }
}

public class ucFoo2 : UserControl
{
    private Panel pnlFooContainer = new Panel();

    public ucFoo2()
    {
         this.Controls.Add(pnlFooContainer);
         Thread t = new Thread(new ThreadStart(AddFooControlToFooConatiner());
         t.Start()
    }

    private AddFooControlToFooConatiner()
    {
         ucFoo foo = new ucFoo();
         this.pnlFooContainer.Controls.Add(ucFoo); //<-- this is where the exception is raised
    }
}

以下是程序: public class ucFoo : UserControl { private Panel pnlFoo = new Panel(); public ucFoo() { this.Controls.Add(pnlFoo); } } public class ucFoo2 : UserControl { private Panel pnlFooContainer = new Panel(); public ucFoo2() { this.Controls.Add(pnlFooContainer); Thread t = new Thread(new ThreadStartAddFooControlToFooConatiner()); t.Start(); } private AddFooControlToFooConatiner() { ucFoo foo = new ucFoo(); this.pnlFooContainer.Controls.Add(ucFoo); //<-- 这是引发异常的地方 } } - Nissim
我将代码片段添加为答案,以便更好地阅读。 - Nissim
7个回答

3

顺带一提 - 为了避免创建无数的委托类型:

if (panel.InvokeRequired)
{
    panel.Invoke((MethodInvoker) delegate { AddControlToPanel(panel,ctrl); } );
    return;
}

此外,现在对内部调用AddControlToPanel进行常规静态检查,因此您不会出错。

1
尝试过了,但仍然出现相同的错误。 问题在于一旦调用面板,就需要调用ctrl。 - Nissim
哦,我明白了 - 我完全同意Jon的分析(即您在工作线程上创建控件) - 我只是想展示一些技巧来节省一些打字并改善静态检查。 - Marc Gravell

3

pnlFoo在哪个线程中创建?您知道它的句柄是什么时候被创建的吗?如果它是在原始(非UI)线程中创建的,那就是问题所在。

同一窗口中的所有控件句柄都应该在同一线程中创建和访问。此时,您不需要对是否需要调用Invoke进行两次检查,因为ctrlpanel应该使用相同的线程。

如果这没有帮助,请提供一个简短但完整的程序来演示问题。


1
这并不完全正确。控件对象可以在任何线程上创建,但它的句柄不能在不同的线程上创建。由于OP实际上正在处理这个非常细节的问题,所以这个区别更加重要。 - Remus Rusanu
请翻译以下关于编程的内容,将翻译后的文本返回:(如果您可以检查编辑是否正确,那将非常有帮助。) - Jon Skeet
我完全相信操作者不应该以它目前的方式进行处理。显然,依赖于托管C#封装和底层GDI句柄之间的线程差异是过于晦涩和复杂的,每个人在处理该代码时都会陷入同样的困境。 - Remus Rusanu

3
'panel'和'ctrl'必须在同一线程上创建,即panel.InvokeRequired不能返回与ctrl.InvokeRequired不同的值。这是指如果panel和ctrl都已创建句柄或属于已创建句柄的容器。来自MSDN
如果控件的句柄尚不存在,则InvokeRequired会向上搜索控件的父级链,直到找到具有窗口句柄的控件或窗体为止。如果找不到适当的句柄,则InvokeRequired方法返回false。
现在你的代码存在竞态条件,因为panel.InvokeNeeded可能会返回false,因为panel尚未创建,然后ctrl.InvokeNeeded肯定会返回false,因为大多数情况下ctrl尚未添加到任何容器中,然后当你到达panel.Controls.Add时,panel在主线程中已创建,所以调用将失败。

那么最好的方法是什么? - Nissim
这取决于许多因素。如果在运行时从容器中添加或移除面板(因此其句柄被销毁),也可能会出现问题。因此,我认为一个安全的方法是有一个额外的参数,可能是主窗体,它保证被创建,并且应该用于跨线程检查和调用。只有在创建了这个窗体之后,才应该启动后台线程。 - Remus Rusanu
顺便说一句,我也同意其他帖子的观点,即你可能不应该做你正在做的事情,尝试在后台线程中创建控制对象并在主线程中实现句柄。相反,应该让后台线程将数据传递到主线程,并始终在主线程中创建控件。 - Remus Rusanu

1

这里是一段可用的代码:

public delegate void AddControlToPanelDlg(Panel p, Control c);

        private void AddControlToPanel(Panel p, Control c)
        {
            p.Controls.Add(c);
        }

        private void AddNewContol(object state)
        {
            object[] param = (object[])state;
            Panel p = (Panel)param[0];
            Control c = (Control)param[1]
            if (p.InvokeRequired)
            {
                p.Invoke(new AddControlToPanelDlg(AddControlToPanel), p, c);
            }
            else
            {
                AddControlToPanel(p, c);
            }
        }

这是我测试的方法。您需要一个带有2个按钮和一个flowLayoutPanel的表单(我选择了这个,因此在面板中动态添加控件时不必关心位置)。
private void button1_Click(object sender, EventArgs e)
        {
            AddNewContol(new object[]{flowLayoutPanel1, CreateButton(DateTime.Now.Ticks.ToString())});
        }

        private void button2_Click(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(AddNewContol), new object[] { flowLayoutPanel1, CreateButton(DateTime.Now.Ticks.ToString()) });
        }

我认为你的例子存在问题,当你进入InvokeRequired分支时,你调用了同一个函数,导致了一种奇怪的递归情况。


尝试过了...它实际上是普通调用的更高级的同义词...但正如我所提到的,一旦“panel”被调用 - “ctrl” InvokeRequired=true - Nissim
不确定该说什么,对我来说它运行良好。你的代码和我上面粘贴的完全一样吗?有时候最小的细节也可能导致错误。 - AlexDrenea

1

这里有很多有趣的答案,但对于在Winform应用程序中进行任何多线程操作来说,一个关键要素是使用BackgroundWorker来启动线程,并与主Winform线程进行通信。


1
在你自己的回答中,你说:
“为了澄清事情:‘panel’是在基础线程上创建的,而‘ctrl’是在新线程上创建的。”
我认为这可能是你问题的原因。所有UI元素都应该在同一个线程(基础线程)上创建。如果你需要在新线程中创建“ctrl”,那么就向基础线程触发一个事件,并在那里进行创建。

0

这是完整程序的一个小片段:

public class ucFoo : UserControl
{
    private Panel pnlFoo = new Panel();

    public ucFoo()
    {
        this.Controls.Add(pnlFoo);
    }
}

public class ucFoo2 : UserControl
{
    private Panel pnlFooContainer = new Panel();

    public ucFoo2()
    {
         this.Controls.Add(pnlFooContainer);
         Thread t = new Thread(new ThreadStart(AddFooControlToFooConatiner());
         t.Start()
    }

    private AddFooControlToFooConatiner()
    {
         ucFoo foo = new ucFoo();
         this.pnlFooContainer.Controls.Add(ucFoo); //<-- this is where the exception is raised
    }
}

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