跨线程操作无效:从创建它的线程以外的线程访问控件。

673
我有一个情景(Windows Forms,C#,.NET):
1. 有一个主窗体,其中包含一些用户控件。 2. 用户控件执行一些繁重的数据操作,如果直接调用“UserControl_Load”方法,则UI在加载方法执行期间会变得无响应。 3. 为了克服这个问题,我在不改变现有代码的情况下在不同的线程上加载数据。 4. 我使用了一个后台工作线程来加载数据,并在完成时通知应用程序它已经完成了它的工作。 5. 现在出现了一个真正的问题。所有的UI(主窗体及其子用户控件)都是在主线程上创建的。在用户控件的LOAD方法中,我根据用户控件上某些控件(如文本框)的值获取数据。
伪代码如下: CODE 1
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.
    }
}

异常信息如下:

跨线程操作无效: 访问控件时引用了一个不是创建控件的线程。

为了更好地了解该问题,我进行了一些谷歌搜索,并得出了使用以下代码的建议:

代码2:

UserContrl1_LoadDataMethod()
{
    if (InvokeRequired) // Line #1
    {
        this.Invoke(new MethodInvoker(UserContrl1_LoadDataMethod));
        return;
    }

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

但是似乎我又回到了原点。应用程序再次变得无响应。这似乎是由于执行第一行if条件语句所致。加载任务再次由父线程而不是我创建的第三个线程完成。
我不知道我是否理解正确。
我该如何解决这个问题,执行第一行if块的效果是什么?
情况是这样的:我想根据控件的值将数据加载到全局变量中。我不想从子线程更改控件的值。我永远不会从子线程做这件事。
因此,只访问该值以便从数据库中获取相应的数据。

对于我遇到的这个错误,我发现解决方法是在窗体上使用BackgroundWorker来处理代码中耗费大量数据的部分。(即将所有问题代码放入backgroundWorker1_DoWork()方法中,并通过backgroundWorker1.RunWorkerAsync()调用它)... 这两个来源指引了我正确的方向:https://dev59.com/IG445IYBdhLWcg3wiq8i https://www.youtube.com/watch?v=MLrrbG6V1zM - Giollia
22个回答

482

根据Prerak K的更新评论(已被删除):

我想我没有正确提出问题。

情况是这样的:我想根据控件的值加载数据到全局变量中。我不想从子线程更改控件的值。我永远不会从子线程进行更改。

所以只访问该值,以便可以从数据库中获取相应的数据。

那么您想要的解决方案应该如下:

UserContrl1_LOadDataMethod()
{
    string name = "";
    if(textbox1.InvokeRequired)
    {
        textbox1.Invoke(new MethodInvoker(delegate { name = textbox1.text; }));
    }
    if(name == "MyName")
    {
        // do whatever
    }
}

在尝试切换回控件线程之前,请将重要的处理工作放在单独的线程中完成。例如:


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

1
我已经有一段时间没有做C#编程了,但根据MSDN文章和我的零散知识,看起来是这样的。 - Jeff Hubbard
1
区别在于,BeginInvoke() 是异步的,而 Invoke() 则是同步运行的。https://dev59.com/zXVC5IYBdhLWcg3woStW - frzsombor

223

UI中的线程模型

请阅读UI应用程序中的线程模型旧的VB链接在此),以了解基本概念。该链接导航到描述WPF线程模型的页面。但是,Windows Forms也使用了相同的思想。

UI线程

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

enter image description here

如果UI线程没有工作,则可以利用空闲时间进行非UI相关的计算。为了利用这些空闲时间,请使用System.Windows.Forms.Control.InvokeSystem.Windows.Forms.Control.BeginInvoke方法:

enter image description here

BeginInvoke和Invoke方法

enter image description here

调用

enter image description here

BeginInvoke

enter image description here

代码解决方案

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


这是WPF线程模型的更新链接:https://learn.microsoft.com/zh-cn/dotnet/framework/wpf/advanced/threading-model。 - Adam Howell

75

你只需要使用InvokeBeginInvoke来更新UI的最小工作量。你的"重型"方法应该在另一个线程上执行(例如通过BackgroundWorker),然后再使用Control.Invoke/Control.BeginInvoke来更新UI。这样,你的UI线程将能够处理UI事件等。

请参阅我的线程文章,其中包括一个WinForms示例——尽管该文章是在BackgroundWorker出现之前编写的,我恐怕在这方面没有进行更新。 BackgroundWorker仅使回调稍微简单了一些。


在我的这种情况下,我甚至没有改变用户界面。我只是从子线程访问其当前值。有什么建议如何实现? - Prerak K
1
即使只是访问属性,您仍然需要跨越到UI线程。如果您的方法在访问值之前无法继续执行,则可以使用返回该值的委托。但是,请通过UI线程进行操作。 - Jon Skeet
嗨Jon,我相信你正在引导我朝着正确的方向前进。是的,我需要这个值,否则我无法继续进行。请问你能详细说明一下“使用返回值的委托”吗?谢谢。 - Prerak K
1
使用委托,例如Func<string>:string text = textbox1.Invoke((Func<string>) () => textbox1.Text);(假设您正在使用C# 3.0 - 否则可以使用匿名方法。) - Jon Skeet

69

我知道现在已经太晚了。但是即使今天您仍然无法访问跨线程控件?这是迄今为止最简短的答案 :P

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

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


2
这个问题提示我:直到窗口句柄被创建之前,不能在控件上调用Invoke or BeginInvoke。我在这里解决了它here - rupweb
我们如何从另一个类中访问"label1.Text"以执行调用。 - Deniz
爱你!太棒了! - undefined

45

我曾经遇到过使用 FileSystemWatcher 时出现的问题,后来发现以下代码可以解决这个问题:

fsw.SynchronizingObject = this

这段代码会让控件使用当前窗体对象来处理事件,因此在同一线程上执行。


3
这拯救了我。在VB.NET中,我使用了“.SynchronizingObject = Me”。 - codingcoding

22
我发现与表单相关的所有方法中需要散布检查和调用代码,这样做过于冗长且不必要。下面是一个简单的扩展方法,可以让你完全摆脱它:
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);
    }
}

然后您只需这样做:

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

没有更多的折腾 - 简单易懂。

18

在.NET中,控件通常不是线程安全的。这意味着您不应该从不同于其所属线程的线程访问控件。为了解决这个问题,您需要调用控件,这就是您的第二个示例尝试做的事情。

然而,在您的情况下,您所做的所有操作都只是将长时间运行的方法返回给主线程。当然,这并不是您真正想要做的。您需要重新考虑一下,以便在主线程上只设置一些快速属性。


15

10

按照我认为最简单的方式,跨线程修改对象:

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

10
使用异步/等待和回调的新方式。如果在项目中保留扩展方法,则只需要一行代码。
/// <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、自动异常记录和回调函数。
    /// <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
        }
    }

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