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


584

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

  1. 有一个主窗体可以承载一些用户控件。
  2. 用户控件执行一些繁重的数据操作,因此,如果我直接调用该UserControl_Load方法,则UI将在装入方法执行期间无响应。
  3. 为了克服这个问题,我将数据加载到不同的线程上(尝试尽我所能更改现有代码)
  4. 我使用了一个后台工作线程来加载数据,完成后将通知应用程序它已经完成了工作。
  5. 现在出现了一个真正的问题。所有UI(主窗体及其子用户控件)均在主主线程上创建。在usercontrol的LOAD方法中,我基于userControl上某些控件(如文本框)的值获取数据。

伪代码如下所示:

代码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 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行。加载任务再次由父线程完成,而不是我产生的第三个任务。

我不知道我是对是错。我是线程新手。

我该如何解决这个问题,如果执行第1行的代码阻塞还会产生什么影响?

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

因此,仅访问该值,以便可以从数据库中获取相应的数据。


对于此错误的特定实例,我发现解决方法是在表单上使用BackgroundWorker来处理代码中数据密集型部分。(即,将所有问题代码放入backgroundWorker1_DoWork()方法中,并通过backgroundWorker1.RunWorkerAsync()进行调用)...这两个来源向我指出了正确的方向:stackoverflow.com/questions/4806742/… youtube.com/ v = MLrrbG6V1zM
Giollia

Answers:


433

根据Prera​​k 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文章和我的零碎知识,它看起来像它。
杰夫·哈伯德

1
区别在于,BeginInvoke()是异步的,而Invoke()是同步运行的。stackoverflow.com/questions/229554/...
frzsombor

178

UI中的线程模型

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

UI线程

在此处输入图片说明

在此处输入图片说明

BeginInvoke和Invoke方法

在此处输入图片说明

调用

在此处输入图片说明

BeginInvoke

在此处输入图片说明

代码解决方案

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



72

您只想使用InvokeBeginInvoke完成更改UI所需的最少工作。您的“重载”方法应在另一个线程上执行(例如通过BackgroundWorker),然后使用Control.Invoke/ Control.BeginInvoke只是更新UI。这样,您的UI线程将可以自由处理UI事件等。

请参阅我的线程文章中的WinForms示例 -尽管该文章是BackgroundWorker在现场出现之前编写的,但恐怕我在这方面还没有进行更新。BackgroundWorker只是稍微简化了回调。


在我这种情况下。我什至没有改变UI。我只是从子线程访问它的当前值。实施的任何建议
Prera​​k K 08/09/26

1
您仍然需要封送UI线程,甚至只是为了访问属性。如果您的方法在访问该值之前无法继续,则可以使用返回该值的委托。但是,可以,通过UI线程。
乔恩·斯基特

乔恩,您好,我相信您正在朝正确的方向前进。是的,我需要没有它的价值,我无法继续前进。请您详细介绍一下“使用返回值的委托”。谢谢
Prera​​k K 08/09/26

1
使用诸如Func <string>之类的委托:string text = textbox1.Invoke((Func <string>)()=> textbox1.Text); (假设您使用的是C#3.0-否则可以使用匿名方法。)
Jon Skeet

45

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

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

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


1
这给了我Invoke or BeginInvoke cannot be called on a control until the window handle has been created。我在这里
rupweb

42

我遇到了这个问题,FileSystemWatcher发现以下代码解决了该问题:

fsw.SynchronizingObject = this

控件然后使用当前的表单对象来处理事件,因此将在同一线程上。


2
这节省了我的培根。在VB.NET中,我使用了.SynchronizingObject = Me

20

我发现在所有与表单相关的方法中都需要乱七八糟的检查和调用代码,以至于过于冗长和不必要。这是一个简单的扩展方法,可让您完全消除它:

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

没有更多的混乱-简单。


这里不是什么
-Rawat

@Rawat t在这种情况下将是textbox1-它作为参数传递
Rob

17

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

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



10

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

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

您可以将其他内容添加到Extension方法中,例如将其包装在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
        }
    }

10

这不是解决此错误的推荐方法,但是您可以快速消除它,它将起作用。对于原型或演示,我更喜欢这样做。加

CheckForIllegalCrossThreadCalls = false

Form1()构造函数中。


9

按照最简单的方法(我认为),可以从另一个线程修改对象:

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

很简单!谢谢 。
Ali Esmaeili


7

在xamarin stuidio之外的visual studio winforms原型项目中为iOS-Phone monotouch应用程序控制器编程时,我发现需要这样做。我希望尽可能在VS over xamarin Studio上进行编程,我希望控制器与电话框架完全分离。通过这种方式为其他框架(如Android和Windows Phone)实现此功能,以后使用会容易得多。

我想要一种解决方案,其中GUI可以响应事件,而无需承担每次单击按钮后的交叉线程切换代码的​​负担。基本上让类控制器进行处理,以使客户端代码保持简单。您可能在GUI上发生了许多事件,好像您可以在类中的一个地方进行处理一样,会更干净。我不是多学科专家,请告诉我这是否有缺陷。

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窗体不知道控制器正在运行异步任务。

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

6

如果您正在使用的对象没有,这是另一种方法

(InvokeRequired)

如果您使用的是主窗体中的对象,但没有InvokeRequired,则该主窗体在主窗体以外的类中使用时,这很有用

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的对象,但是可以访问MainForm,则它是不同的方法


5

与以前的答案相同,只是增加了很短的内容,允许使用所有Control属性而没有跨线程调用异常。

辅助方法

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

样品用量

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


5

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

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

3

同样的问题:如何从C中的另一个线程更新GUI

两种方式:

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

  2. 声明一些变量以将这些类型的值保存在单独的类中(它将作为数据持有者使用)。创建此类的静态实例,您可以通过任何线程访问它。

例:

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



0

解决此问题的简单且可重复使用的方法。

扩展方式

public static class FormExts
{
    public static void LoadOnUI(this Form frm, Action action)
    {
        if (frm.InvokeRequired) frm.Invoke(action);
        else action.Invoke();
    }
}

样品用量

private void OnAnyEvent(object sender, EventArgs args)
{
    this.LoadOnUI(() =>
    {
        label1.Text = "";
        button1.Text = "";
    });
}

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.