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

我有一个设想。(Windows Forms, c#, .NET)

  1. 有一个主窗体,它承载一些用户控件。
  2. 用户控件执行一些繁重的数据操作,例如,如果我直接调用UserControl_Load方法,则UI在load方法执行期间变得无响应。
  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 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
}
}
但是我似乎还是回到了原点。再次申请 变得反应迟钝。这似乎是由于第1行if条件的执行。加载任务再次由父线程完成,而不是由我生成的第三个线程完成

我不知道我认为这是对还是错。我不熟悉穿线。

我如何解决这个问题,以及执行第1行if块的效果是什么?

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

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

476888 次浏览

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

然而,在您的例子中,您所做的只是将长时间运行的方法传递回主线程。当然,这并不是你真正想做的。你需要重新思考这一点,以便你在主线程上所做的一切都是在这里和那里设置一个快速属性。

您只需要使用InvokeBeginInvoke来完成更改UI所需的最小工作量。你的“heavy”方法应该在另一个线程上执行(例如通过BackgroundWorker),然后使用Control.Invoke/Control.BeginInvoke来更新UI。这样你的UI线程就可以自由地处理UI事件等。

看看我的线程的文章 WinForms的例子 -尽管这篇文章是在BackgroundWorker到达现场之前写的,恐怕我还没有在这方面更新它。BackgroundWorker只是稍微简化了一下回调。

根据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;
}
}
}
你需要看看Backgroundworker的例子:
http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx 特别是它如何与UI层交互。根据你的帖子,这似乎回答了你的问题

我有这个问题与FileSystemWatcher和发现下面的代码解决了这个问题:

# EYZ0

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

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

这里有一种替代方法,如果您正在处理的对象没有

(InvokeRequired)

如果您使用的是类中的主窗体,而不是主窗体中的对象,但该对象在主窗体中,但没有invokerrequired,那么这是非常有用的

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,那么它是一种不同的方法

当我在xamarin工作室外的一个visual studio winforms原型项目中编程iOS-Phone单触摸应用程序控制器时,我发现了这个需求。比起xamarin studio,我更倾向于在VS中编程,我希望控制器能够与手机框架完全分离。通过这种方式,在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);
}
}
}
}

UI中的线程模型

为了理解基本概念,请阅读UI应用程序中的线程模型 (旧的VB链接在这里)。该链接导航到描述WPF线程模型的页面。但是,Windows窗体利用了相同的思想。

UI线程

  • 只有一个线程(UI线程)被允许访问System.Windows.Forms.Control及其子类成员。
  • 尝试从不同的线程访问System.Windows.Forms.Control成员,而不是UI线程将导致跨线程异常。
  • 因为只有一个线程,所以所有的UI操作都作为工作项进入该线程:

enter image description here

enter image description here

BeginInvoke和Invoke方法

enter image description here

调用

enter image description here

BeginInvoke

enter image description here

代码的解决方案

阅读问题如何更新GUI从另一个线程在c# ?的答案。 对于c# 5.0和。net 4.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

与之前的答案相同, 但是一个非常简短的添加,允许使用所有的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;
}

我发现需要在与表单相关的所有方法中随意放置的检查和调用代码太冗长且不需要。下面是一个简单的扩展方法,可以让你完全摆脱它:

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

没有更多的混乱-简单。

遵循最简单的(在我看来)方法来修改来自另一个线程的对象:

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

使用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语句包装它,允许调用者告诉它在完成后返回什么类型,一个异常回调给调用者:

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

同样的问题:how-to-update-the-gui from-another-thread-in-c

两种方式:

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

这不是解决此错误的推荐方法,但您可以快速抑制它,它将完成工作。对于原型或演示,我更喜欢这样。添加

CheckForIllegalCrossThreadCalls = false

Form1()构造函数中。

我知道现在太晚了。然而,即使在今天,如果你有问题访问跨线程控件?这是迄今为止最短的答案:P

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

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

this.Invoke(new MethodInvoker(delegate
{
//your code here;
}));

y行动;//在类中声明

label1.Invoke (y = () = > label1.Text =“文本”);

跨线程操作有两个选项。

Control.InvokeRequired Property

第二个是使用

SynchronizationContext Post Method

控制。InvokeRequired只在从Control类继承控件时有用,而SynchronizationContext可以在任何地方使用。一些有用的信息如下链接

# EYZ0

跨线程更新UI using SynchronizationContext | .Net .Net

简单地使用这个:

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

简单和可重用的方法来解决这个问题。

扩展方法

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