关于c#:跨线程操作无效:从创建它的线程以外的线程访问控件

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

我有一个场景。(Windows窗体,C,.NET)

  • 有一个主窗体承载一些用户控件。
  • 用户控件执行一些繁重的数据操作,例如,如果我直接调用UserControl_Load方法,则在执行加载方法期间,UI将变得不响应。
  • 为了克服这个问题,我在不同的线程上加载数据(尽可能少地更改现有代码)
  • 我使用了一个后台工作线程,它将加载数据,完成后将通知应用程序它已经完成了它的工作。
  • 现在出现了一个真正的问题。所有UI(主窗体及其子用户控件)都是在主主线程上创建的。在UserControl的Load方法中,我根据UserControl上的某个控件(如TextBox)的值获取数据。
  • 伪代码如下所示:

    代码1

    1
    2
    3
    4
    5
    6
    7
    8
    UserContrl1_LoadDataMethod()
    {
        if (textbox1.text =="MyName") // This gives exception
        {
            //Load data corresponding to"MyName".
            //Populate a globale variable List<string> which will be binded to grid at some later stage.
        }
    }

    它给出的例外是

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

    为了了解更多关于这方面的信息,我做了一些谷歌搜索,并提出了一个建议,比如使用以下代码

    代码2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    UserContrl1_LoadDataMethod()
    {
        if (InvokeRequired) // Line #1
        {
            this.Invoke(new MethodInvoker(UserContrl1_LoadDataMethod));
            return;
        }

        if (textbox1.text =="MyName") // Now it wont give an exception
        {
        //Load data correspondin to"MyName"
            //Populate a globale variable List<string> which will be binded to grid at some later stage
        }
    }

    但是但是…看来我又回到正题了。再次申请变得毫无反应。这似乎是由于执行第1行if条件所致。加载任务再次由父线程完成,而不是我生成的第三个线程。

    我不知道我是否意识到这是对的还是错的。我不熟悉穿线。

    如何解决此问题,以及执行第1行if块的效果如何?

    情况是这样的:我希望根据控件的值将数据加载到全局变量中。我不想从子线程更改控件的值。我永远不会从孩子的角度去做。

    因此,只有访问该值,才能从数据库中提取相应的数据。


    根据Prerak K的更新评论(自删除后):

    I guess I have not presented the question properly.

    Situation is this: I want to load data into a global variable based on the value of a control. I don't want to change the value of a control from the child thread. I'm not going to do it ever from a child thread.

    So only accessing the value so that corresponding data can be fetched from the database.

    然后,您需要的解决方案应如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    UserContrl1_LOadDataMethod()
    {
        string name ="";
        if(textbox1.InvokeRequired)
        {
            textbox1.Invoke(new MethodInvoker(delegate { name = textbox1.text; }));
        }
        if(name =="MyName")
        {
            // do whatever
        }
    }

    在尝试切换回控件的线程之前,请在单独的线程中进行认真的处理。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    UserContrl1_LOadDataMethod()
    {
        if(textbox1.text=="MyName") //<<======Now it wont give exception**
        {
            //Load data correspondin to"MyName"
            //Populate a globale variable List<string> which will be
            //bound to grid at some later stage
            if(InvokeRequired)
            {
                // after we've done all the processing,
                this.Invoke(new MethodInvoker(delegate {
                    // load the control with the appropriate data
                }));
                return;
            }
        }
    }


    型UI中的线程模型

    为了理解基本概念,请阅读UI应用程序中的线程模型。链接导航到描述WPF线程模型的页面。然而,Windows窗体使用相同的思想。

    用户界面线程

      百万千克1只有一个线程(UI线程)可以访问System.Windows.Forms.Control及其子类成员。百万千克1百万千克1尝试从不同于UI线程的线程访问System.Windows.Forms.Control的成员将导致跨线程异常。百万千克1百万千克1由于只有一个线程,所有UI操作都作为工作项排队进入该线程:百万千克1

    enter image description here

      百万千克1如果没有针对UI线程的工作,那么就存在空闲间隙,可以由与UI无关的计算使用。百万千克1百万千克1要使用上述间隙,请使用System.Windows.Forms.Control.Invoke或System.Windows.Forms.Control.BeginInvoke方法:百万千克1

    氧化镁

    BeginInvoke和Invoke方法

      百万千克1正在调用的方法的计算开销应该很小,并且事件处理程序方法的计算开销也应该很小,因为那里使用的是UI线程——与负责处理用户输入的线程相同。无论这是System.Windows.Forms.Control.Invoke还是System.Windows.Forms.Control.BeginInvoke。百万千克1百万千克1要执行计算昂贵的操作,请始终使用单独的线程。因为.NET 2.0 BackgroundWorker致力于在Windows窗体中执行昂贵的计算操作。但是,在新的解决方案中,您应该使用这里描述的异步等待模式。百万千克1百万千克1仅使用System.Windows.Forms.Control.Invoke或System.Windows.Forms.Control.BeginInvoke方法更新用户界面。如果将它们用于大量计算,应用程序将阻止:百万千克1

    氧化镁

    援引

      百万千克1System.Windows.Forms.Control.Invoke导致单独的线程等待调用的方法完成:百万千克1

    氧化镁

    异步调用

      百万千克1System.Windows.Forms.Control.BeginInvoke不会导致单独的线程等待调用的方法完成:百万千克1

    氧化镁

    代码解决方案

    阅读有关如何从C中的另一个线程更新GUI的问题的答案?.对于C 5.0和.NET 4.5,这里是推荐的解决方案。


    您只想使用invoke或begininvoke来完成更改UI所需的最少的工作。您的"heavy"方法应该在另一个线程上执行(例如,通过backgroundworker),但随后使用control.invoke/control.begininvoke来更新UI。这样,您的UI线程就可以自由地处理UI事件等。

    请参阅我的线程文章中的一个WinForms示例-尽管这篇文章是在BackgroundWorker到达现场之前编写的,但恐怕我还没有在这方面进行更新。后台工作人员只是稍微简化了回调。


    我在FileSystemWatcher上遇到了这个问题,发现下面的代码解决了这个问题:

    fsw.SynchronizingObject = this

    然后,控件使用当前窗体对象来处理事件,因此将位于同一线程上。


    我知道现在太迟了。然而,即使在今天,如果您在访问跨线程控件时遇到问题?这是迄今为止最短的答案:p

    1
    2
    3
    4
    Invoke(new Action(() =>
                    {
                        label1.Text ="WooHoo!!!";
                    }));

    这就是我如何从线程访问任何表单控件。


    我发现与表单相关的所有方法中都需要散乱的检查和调用代码太过冗长和不必要。下面是一个简单的扩展方法,可以让您完全摆脱它:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static class Extensions
    {
        public static void Invoke<TControlType>(this TControlType control, Action<TControlType> del)
            where TControlType : Control
            {
                if (control.InvokeRequired)
                    control.Invoke(new Action(() => del(control)));
                else
                    del(control);
        }
    }

    然后你可以简单地做到:

    1
    textbox1.Invoke(t => t.Text ="A");

    别再胡闹了-简单。


    .NET中的控件通常不是线程安全的。这意味着你不应该从一个线程访问一个控件,而不是从它所在的线程访问。为了解决这个问题,您需要调用控件,这正是您的第二个示例所尝试的。

    但是,在您的情况下,您所做的就是将长时间运行的方法传递回主线程。当然,这不是你真正想做的。您需要重新考虑一下,这样您在主线程上所做的就是在这里和那里设置一个快速属性。


    针对UI跨线程问题最干净(也是最合适)的解决方案是使用SynchronizationContext,请参阅多线程应用程序文章中的"同步对UI的调用",它很好地解释了这一点。


    使用异步/等待和回调的新外观。如果在项目中保留扩展方法,则只需要一行代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /// <summary>
    /// A new way to use Tasks for Asynchronous calls
    /// </summary>
    public class Example
    {
        /// <summary>
        /// No more delegates, background workers etc. just one line of code as shown below
        /// Note it is dependent on the XTask class shown next.
        /// </summary>
        public async void ExampleMethod()
        {
            //Still on GUI/Original Thread here
            //Do your updates before the next line of code
            await XTask.RunAsync(() =>
            {
                //Running an asynchronous task here
                //Cannot update GUI Thread here, but can do lots of work
            });
            //Can update GUI/Original thread on this line
        }
    }

    /// <summary>
    /// A class containing extension methods for the Task class
    /// Put this file in folder named Extensions
    /// Use prefix of X for the class it Extends
    /// </summary>
    public static class XTask
    {
        /// <summary>
        /// RunAsync is an extension method that encapsulates the Task.Run using a callback
        /// </summary>
        /// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
        /// <returns></returns>
        public async static Task RunAsync(Action Code)
        {
            await Task.Run(() =>
            {
                Code();
            });
            return;
        }
    }

    您可以向扩展方法添加其他内容,例如将其包装在try/catch语句中,允许调用方告诉它完成后返回的类型,调用方的异常回调:

    添加try catch、自动异常日志记录和回调

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
        /// <summary>
        /// Run Async
        /// </summary>
        /// <typeparam name="T">The type to return</typeparam>
        /// <param name="Code">The callback to the code</param>
        /// <param name="Error">The handled and logged exception if one occurs</param>
        /// <returns>The type expected as a competed task</returns>

        public async static Task<T> RunAsync<T>(Func<string,T> Code, Action<Exception> Error)
        {
           var done =  await Task<T>.Run(() =>
            {
                T result = default(T);
                try
                {
                   result = Code("Code Here");
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Unhandled Exception:" + ex.Message);
                    Console.WriteLine(ex.StackTrace);
                    Error(ex);
                }
                return result;

            });
            return done;
        }
        public async void HowToUse()
        {
           //We now inject the type we want the async routine to return!
           var result =  await RunAsync<bool>((code) => {
               //write code here, all exceptions are logged via the wrapped try catch.
               //return what is needed
               return someBoolValue;
           },
           error => {

              //exceptions are already handled but are sent back here for further processing
           });
            if (result)
            {
                //we can now process the result because the code above awaited for the completion before
                //moving to this statement
            }
        }


    你需要看看后台工作者的例子:http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx尤其是它如何与UI层交互。根据你的帖子,这似乎可以回答你的问题。


    按照最简单的方法(在我看来)从另一个线程修改对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    using System.Threading.Tasks;
    using System.Threading;

    namespace TESTE
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }

            private void button1_Click(object sender, EventArgs e)
            {
                Action<string> DelegateTeste_ModifyText = THREAD_MOD;
                Invoke(DelegateTeste_ModifyText,"MODIFY BY THREAD");
            }

            private void THREAD_MOD(string teste)
            {
                textBox1.Text = teste;
            }
        }
    }


    这不是解决此错误的建议方法,但您可以快速抑制它,它将完成此任务。我更喜欢这个用于原型或演示。添加

    1
    CheckForIllegalCrossThreadCalls = false

    Form1()构造函数中。


    在Xamarin Studio之外的Visual Studio Winforms原型项目中,我在编写iOS Phone MonoTouch应用程序控制器时发现了这一需求。比起Xamarin Studio,我更喜欢在vs中编程,我希望控制器与电话框架完全分离。这种方式在其他框架(如android和windows phone)中实现这一点对于将来的使用将更加容易。

    我想要一个解决方案,在这个解决方案中,GUI可以响应事件,而不必在每次点击按钮后处理跨线程切换代码。基本上,让类控制器处理它,以保持客户机代码简单。您可能在GUI上有许多事件,在这些事件中,您可以在类中的一个地方处理它,这样做会更干净。我不是一个多面手,如果这是有缺陷的请告诉我。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public partial class Form1 : Form
    {
        private ExampleController.MyController controller;

        public Form1()
        {          
            InitializeComponent();
            controller = new ExampleController.MyController((ISynchronizeInvoke) this);
            controller.Finished += controller_Finished;
        }

        void controller_Finished(string returnValue)
        {
            label1.Text = returnValue;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            controller.SubmitTask("Do It");
        }
    }

    GUI窗体不知道控制器正在运行异步任务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public delegate void FinishedTasksHandler(string returnValue);

    public class MyController
    {
        private ISynchronizeInvoke _syn;
        public MyController(ISynchronizeInvoke syn) {  _syn = syn; }
        public event FinishedTasksHandler Finished;

        public void SubmitTask(string someValue)
        {
            System.Threading.ThreadPool.QueueUserWorkItem(state => submitTask(someValue));
        }

        private void submitTask(string someValue)
        {
            someValue = someValue +"" + DateTime.Now.ToString();
            System.Threading.Thread.Sleep(5000);
    //Finished(someValue); This causes cross threading error if called like this.

            if (Finished != null)
            {
                if (_syn.InvokeRequired)
                {
                    _syn.Invoke(Finished, new object[] { someValue });
                }
                else
                {
                    Finished(someValue);
                }
            }
        }
    }

    如果您正在处理的对象没有

    1
    (InvokeRequired)

    如果您使用的是一个类(而不是主窗体)中的主窗体,而该类中的对象是主窗体中的,但没有调用Required,则此选项非常有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    delegate void updateMainFormObject(FormObjectType objectWithoutInvoke, string text);

    private void updateFormObjectType(FormObjectType objectWithoutInvoke, string text)
    {
        MainForm.Invoke(new updateMainFormObject(UpdateObject), objectWithoutInvoke, text);
    }

    public void UpdateObject(ToolStripStatusLabel objectWithoutInvoke, string text)
    {
        objectWithoutInvoke.Text = text;
    }

    它的工作原理与上面相同,但是如果没有invokeRequired对象,但是有权访问主窗体,那么它是一种不同的方法。


    和以前的答案一样,但这是一个非常短的添加,它允许使用所有控件属性而不产生跨线程调用异常。

    帮助者方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /// <summary>
    /// Helper method to determin if invoke required, if so will rerun method on correct thread.
    /// if not do nothing.
    /// </summary>
    /// <param name="c">Control that might require invoking</param>
    /// <param name="a">action to preform on control thread if so.</param>
    /// <returns>true if invoke required</returns>
    public bool ControlInvokeRequired(Control c, Action a)
    {
        if (c.InvokeRequired) c.Invoke(new MethodInvoker(delegate
        {
            a();
        }));
        else return false;

        return true;
    }

    样品使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // usage on textbox
    public void UpdateTextBox1(String text)
    {
        //Check if invoke requied if so return - as i will be recalled in correct thread
        if (ControlInvokeRequired(textBox1, () => UpdateTextBox1(text))) return;
        textBox1.Text = ellapsed;
    }

    //Or any control
    public void UpdateControl(Color c, String s)
    {
        //Check if invoke requied if so return - as i will be recalled in correct thread
        if (ControlInvokeRequired(myControl, () => UpdateControl(c, s))) return;
        myControl.Text = s;
        myControl.BackColor = c;
    }

    1
    2
    3
    4
    this.Invoke(new MethodInvoker(delegate
                {
                    //your code here;
                }));

    例如,要从UI线程的控件中获取文本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Private Delegate Function GetControlTextInvoker(ByVal ctl As Control) As String

    Private Function GetControlText(ByVal ctl As Control) As String
        Dim text As String

        If ctl.InvokeRequired Then
            text = CStr(ctl.Invoke(
                New GetControlTextInvoker(AddressOf GetControlText), ctl))
        Else
            text = ctl.Text
        End If

        Return text
    End Function

    同样的问题:如何从另一个线程中更新gui-in-c

    两种方式:

    百万千克1

    返回e.result中的值,并使用它在BackgroundWorker的RunWorkerCompleted事件中设置您的文本框值

    百万千克1百万千克1

    声明一些变量以将这些值保存在单独的类中(它将作为数据容器工作)。创建这个类的静态实例,您可以通过任何线程访问它。

    百万千克1

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public  class data_holder_for_controls
    {
        //it will hold value for your label
        public  string status = string.Empty;
    }

    class Demo
    {
        public static  data_holder_for_controls d1 = new data_holder_for_controls();
        static void Main(string[] args)
        {
            ThreadStart ts = new ThreadStart(perform_logic);
            Thread t1 = new Thread(ts);
            t1.Start();
            t1.Join();
            //your_label.Text=d1.status; --- can access it from any thread
        }

        public static void perform_logic()
        {
            //put some code here in this function
            for (int i = 0; i < 10; i++)
            {
                //statements here
            }
            //set result in status variable
            d1.status ="Task done";
        }
    }


    只需使用:

    1
    2
    3
    4
    this.Invoke((MethodInvoker)delegate
                {
                    YourControl.Property= value; // runs thread safe
                });


    操作y;//在类内声明

    label1.invoke(y=)=>label1.text="text");


    跨线程操作有两个选项。

    1
    Control.InvokeRequired Property

    第二个是使用

    1
    SynchronizationContext Post Method

    只有当处理从控件类继承的控件时,control.invokeRequired才有用,而SynchronizationContext可以在任何地方使用。一些有用的信息如下链接

    跨线程更新ui.net

    使用SynchronizationContext.NET的跨线程更新UI