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

时间:2023-01-09 10:57:07

I've been struggling with this for quite a while: I have a function designed to add control to a panel with cross-thread handling, the problem is that though the panel and the control are in "InvokeRequired=false" - I get an exception telling me that one of the controls inner controls are accessed from a thread other than the thread it was created on, the snippet goes like this:

我一直在努力解决这个问题:我有一个功能,旨在将控件添加到具有跨线程处理的面板,问题是虽然面板和控件在“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
    }

the exception message goes like this:

异常消息如下:

"Cross-thread operation not valid: Control 'pnlFoo' accessed from a thread other than the thread it was created on"

“跨线程操作无效:控制'pnlFoo'从其创建的线程以外的线程访问”

('pnlFoo' is under ctrl.Controls)

('pnlFoo'在ctrl.Controls下)

How can I add ctrl to panel?!

如何将ctrl添加到面板?!


When the code reaches the "panel.Controls.Add(ctrl);" line - both panel and ctrl "InvokeRequired" property is set to false, the problem is the the controls inside ctrl has "InvokeRequired" set to true. To clarify things: "panel" is created on the base thread and "ctrl" on the new thread, therefore, "panel" has to be invoked (causing "ctrl" to need invoke again). Once both of the invokes are done, it reaches the panel.Controls.Add(ctrl) command (both "panel" and "ctrl" doesn't need invocation in this state)

当代码到达“panel.Controls.Add(ctrl);”时line - panel和ctrl“InvokeRequired”属性设置为false,问题是ctrl中的控件将“InvokeRequired”设置为true。澄清事情:在基本线程上创建“panel”,在新线程上创建“ctrl”,因此,必须调用“panel”(导致“ctrl”再次需要调用)。一旦完成两个调用,它就会到达panel.Controls.Add(ctrl)命令(“panel”和“ctrl”都不需要在这种状态下调用)

Here is a small snippet of the full program:

这是完整程序的一小部分:

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

7 个解决方案

#1


As an aside - to save yourself having to create countless delegate types:

抛开 - 为了节省自己必须创建无数的委托类型:

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

Additionally, this now does regular static checks on the inner call to AddControlToPanel, so you can't get it wrong.

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

#2


'panel' and 'ctrl' must be created on the same thread, ie. you cannot have panel.InvokeRequired return different value than ctrl.InvokeRequired. That is if both panel and ctrl have the handles created or belong to a container with the handle created. From MSDN:

'panel'和'ctrl'必须在同一个线程上创建,即。你不能有panel.InvokeRequired返回不同于ctrl.InvokeRequired的值。也就是说,如果panel和ctrl都创建了句柄,或者属于创建了句柄的容器。来自MSDN:

If the control's handle does not yet exist, InvokeRequired searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false.

如果控件的句柄尚不存在,则InvokeRequired将向上搜索控件的父链,直到找到具有窗口句柄的控件或表单。如果找不到合适的句柄,则InvokeRequired方法返回false。

As it is right now your code is open to race conditions because the panel.InvokeNeeded can return false because the panel is not yet created, then ctrl.InvokeNeeded will certainly return false because most likely ctrl is not yet added to any container and then by the time you reach panel.Controls.Add the panel was created in the main thread, so the call will fail.

因为现在你的代码对竞争条件是开放的,因为panel.InvokeNeeded可以返回false,因为面板尚未创建,然后ctrl.InvokeNeeded肯定会返回false,因为很可能ctrl尚未添加到任何容器然后通过你到达panel.Controls.Add面板是在主线程中创建的,所以调用将失败。

#3


Where is pnlFoo being created, and in which thread? Do you know when its handle is being created? If it's being created in the original (non-UI) thread, that's the problem.

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

All control handles in the same window should be created and accessed on the same thread. At that point, you shouldn't need two checks for whether Invoke is required, because ctrl and panel should be using the same thread.

应在同一个线程上创建和访问同一窗口中的所有控制句柄。此时,您不需要检查是否需要Invoke,因为ctrl和panel应该使用相同的线程。

If this doesn't help, please provide a short but complete program to demonstrate the problem.

如果这没有帮助,请提供简短但完整的程序来证明问题。

#4


Here is a working piece of code :

这是一段代码:

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

And here is how I tested it. You need to have a form with 2 buttons and one flowLayoutPanel (I chose this so I didn't have to care about location hwhen dinamically adding controls in the panel)

这是我测试它的方式。你需要有一个带有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()) });
        }

I that he probem with your exaple is that when you get in the InvokeRequired branch you invoke the same function wou are in, resulting in a strange case of recurssion.

我探究你的问题是,当你进入InvokeRequired分支时,你调用了同样的函数,导致一个奇怪的递归情况。

#5


In your own answer you state:

在你自己的回答中,你说:

To clarify things: "panel" is created on the base thread and "ctrl" on the new thread

澄清事情:在基本线程上创建“panel”,在新线程上创建“ctrl”

I think this might be the cause of your problem. All UI elements should be created on the same thread (the base one). If you need to create "ctrl" as a consequence of some action in the new thread, then fire an event back to the base thread and do the creation there.

我想这可能是你问题的原因。应该在同一个线程(基础线程)上创建所有UI元素。如果由于新线程中的某些操作而需要创建“ctrl”,则将事件激活回基本线程并在那里进行创建。

#6


Lots of interesting answers here, but one key item for any multithreading in a Winform app is using the BackgroundWorker to initiate threads, and communicate back to the main Winform thread.

这里有很多有趣的答案,但Winform应用程序中任何多线程的一个关键项是使用BackgroundWorker启动线程,并与Winform主线程进行通信。

#7


Here is a small snippet of the full program:

这是完整程序的一小部分:

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

#1


As an aside - to save yourself having to create countless delegate types:

抛开 - 为了节省自己必须创建无数的委托类型:

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

Additionally, this now does regular static checks on the inner call to AddControlToPanel, so you can't get it wrong.

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

#2


'panel' and 'ctrl' must be created on the same thread, ie. you cannot have panel.InvokeRequired return different value than ctrl.InvokeRequired. That is if both panel and ctrl have the handles created or belong to a container with the handle created. From MSDN:

'panel'和'ctrl'必须在同一个线程上创建,即。你不能有panel.InvokeRequired返回不同于ctrl.InvokeRequired的值。也就是说,如果panel和ctrl都创建了句柄,或者属于创建了句柄的容器。来自MSDN:

If the control's handle does not yet exist, InvokeRequired searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false.

如果控件的句柄尚不存在,则InvokeRequired将向上搜索控件的父链,直到找到具有窗口句柄的控件或表单。如果找不到合适的句柄,则InvokeRequired方法返回false。

As it is right now your code is open to race conditions because the panel.InvokeNeeded can return false because the panel is not yet created, then ctrl.InvokeNeeded will certainly return false because most likely ctrl is not yet added to any container and then by the time you reach panel.Controls.Add the panel was created in the main thread, so the call will fail.

因为现在你的代码对竞争条件是开放的,因为panel.InvokeNeeded可以返回false,因为面板尚未创建,然后ctrl.InvokeNeeded肯定会返回false,因为很可能ctrl尚未添加到任何容器然后通过你到达panel.Controls.Add面板是在主线程中创建的,所以调用将失败。

#3


Where is pnlFoo being created, and in which thread? Do you know when its handle is being created? If it's being created in the original (non-UI) thread, that's the problem.

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

All control handles in the same window should be created and accessed on the same thread. At that point, you shouldn't need two checks for whether Invoke is required, because ctrl and panel should be using the same thread.

应在同一个线程上创建和访问同一窗口中的所有控制句柄。此时,您不需要检查是否需要Invoke,因为ctrl和panel应该使用相同的线程。

If this doesn't help, please provide a short but complete program to demonstrate the problem.

如果这没有帮助,请提供简短但完整的程序来证明问题。

#4


Here is a working piece of code :

这是一段代码:

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

And here is how I tested it. You need to have a form with 2 buttons and one flowLayoutPanel (I chose this so I didn't have to care about location hwhen dinamically adding controls in the panel)

这是我测试它的方式。你需要有一个带有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()) });
        }

I that he probem with your exaple is that when you get in the InvokeRequired branch you invoke the same function wou are in, resulting in a strange case of recurssion.

我探究你的问题是,当你进入InvokeRequired分支时,你调用了同样的函数,导致一个奇怪的递归情况。

#5


In your own answer you state:

在你自己的回答中,你说:

To clarify things: "panel" is created on the base thread and "ctrl" on the new thread

澄清事情:在基本线程上创建“panel”,在新线程上创建“ctrl”

I think this might be the cause of your problem. All UI elements should be created on the same thread (the base one). If you need to create "ctrl" as a consequence of some action in the new thread, then fire an event back to the base thread and do the creation there.

我想这可能是你问题的原因。应该在同一个线程(基础线程)上创建所有UI元素。如果由于新线程中的某些操作而需要创建“ctrl”,则将事件激活回基本线程并在那里进行创建。

#6


Lots of interesting answers here, but one key item for any multithreading in a Winform app is using the BackgroundWorker to initiate threads, and communicate back to the main Winform thread.

这里有很多有趣的答案,但Winform应用程序中任何多线程的一个关键项是使用BackgroundWorker启动线程,并与Winform主线程进行通信。

#7


Here is a small snippet of the full program:

这是完整程序的一小部分:

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