提问人:Prerak K 提问时间:9/27/2008 最后编辑:starballPrerak K 更新时间:12/29/2022 访问量:499923
跨线程操作无效:从创建控件的线程以外的线程访问控件
Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on
问:
我有一个场景。(Windows 窗体、C#、.NET)
- 有一个主窗体承载一些用户控件。
- 用户控件执行一些繁重的数据操作,因此,如果我直接调用该方法,则 UI 在加载方法执行期间将变得无响应。
UserControl_Load
- 为了克服这个问题,我在不同的线程上加载数据(尝试尽可能少地更改现有代码)
- 我使用了一个后台工作线程,该线程将加载数据,完成后将通知应用程序它已完成工作。
- 现在出现了一个真正的问题。所有 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 条件。加载任务再次由父线程完成,而不是由我生成的第三个线程完成。
我不知道我是否认为这是对还是错。
我该如何解决这个问题,以及执行 Line#1 if 块的影响是什么?
情况是这样的:我想根据控件的值将数据加载到全局变量中。我不想更改子线程中的控件的值。我不会从子线程中执行此操作。
因此,仅访问该值,以便可以从数据库中获取相应的数据。
答:
.NET 中的控件通常不是线程安全的。这意味着不应从控件所在的线程以外的线程访问控件。若要解决此问题,需要调用控件,这是第二个示例正在尝试的控件。
但是,在您的例子中,您所做的只是将长时间运行的方法传递回主线程。当然,这并不是你真正想做的。您需要稍微重新考虑一下这一点,以便您在主线程上所做的只是在这里和那里设置一个快速属性。
您只想使用或用于更改 UI 所需的最低限度的工作。您的“heavy”方法应该在另一个线程上执行(例如通过),然后使用 / 来更新 UI。这样,您的 UI 线程将可以自由地处理 UI 事件等。Invoke
BeginInvoke
BackgroundWorker
Control.Invoke
Control.BeginInvoke
请参阅我的线程文章以获取 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
fsw.SynchronizingObject = this
然后,该控件使用当前窗体对象来处理事件,因此将位于同一线程上。
评论
.SynchronizingObject = Me
UI 跨线程问题最干净(和正确)的解决方案是使用 SynchronizationContext,请参阅在多线程应用程序中同步对 UI 的调用一文,它很好地解释了它。
如果您正在使用的对象没有
(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,则这是一种不同的方法
在 xamarin tuidio 之外的 Visual Studio winforms 原型项目中对 iOS-Phone 单触控应用控制器进行编程时,我发现需要这样做。我更喜欢在 VS 中编程,而不是 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);
}
}
}
}
UI 中的线程模型
请阅读 UI 应用程序中的线程模型(旧的 VB 链接在这里)以了解基本概念。该链接导航到描述 WPF 线程模型的页面。但是,Windows 窗体也采用了相同的理念。
UI 线程
- 只有一个线程(UI 线程)可以访问 System.Windows.Forms.Control 及其子类成员。
- 尝试从与 UI 线程不同的线程访问 System.Windows.Forms.Control 的成员将导致跨线程异常。
- 由于只有一个线程,因此所有 UI 操作都作为工作项排队到该线程中:
- 如果 UI 线程没有工作,则存在空闲间隙,可供非 UI 相关计算使用。
- 若要使用上述间隙,请使用 System.Windows.Forms.Control.Invoke 或 System.Windows.Forms.Control.BeginInvoke 方法:
BeginInvoke 和 Invoke 方法
- 被调用方法的计算开销应该很小,事件处理程序方法的计算开销也应该很小,因为 UI 线程在那里使用 - 负责处理用户输入的线程也是如此。不管这是 System.Windows.Forms.Control.Invoke 还是 System.Windows.Forms.Control.BeginInvoke。
- 要执行计算成本高昂的操作,请始终使用单独的线程。自 .NET 2.0 起,BackgroundWorker 专用于在 Windows 窗体中执行计算成本高昂的操作。但是,在新的解决方案中,应使用此处所述的 async-await 模式。
- 仅使用 System.Windows.Forms.Control.Invoke 或 System.Windows.Forms.Control.BeginInvoke 方法更新用户界面。如果将它们用于繁重的计算,则应用程序将阻止:
调用
- System.Windows.Forms.Control.Invoke 导致单独的线程等待,直到调用的方法完成:
开始调用
- System.Windows.Forms.Control.BeginInvoke 不会导致单独的线程等待,直到调用的方法完成:
代码解决方案
阅读问题的答案:如何在 C# 中从另一个线程更新 GUI?。 对于 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 属性,而不会出现跨线程调用异常。
Helper 方法
/// <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
}
}
两种方式:
在 e.result 中返回值,并使用它来设置事件中的文本框值backgroundWorker_RunWorkerCompleted
声明一些变量以将这些类型的值保存在一个单独的类中(该类将用作数据持有者)。创建此类的静态实例,您可以通过任何线程访问它。
例:
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!!!";
}));
这就是我从线程访问任何表单控件的方式。
评论
Invoke or BeginInvoke cannot be called on a control until the window handle has been created
this.Invoke(new MethodInvoker(delegate
{
//your code here;
}));
行动 y;在类中声明
标签 1。调用(y=()=>label1。text=“文本”);
跨线程操作有两个选项。
Control.InvokeRequired Property
第二个是使用
SynchronizationContext Post Method
Control.InvokeRequired 仅在处理从 Control 类继承的控件时有用,而 SynchronizationContext 可以在任何地方使用。一些有用的信息如下链接
使用 SynchronizationContext 的跨线程更新 UI | .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 = "";
});
}
评论
上一个:在 C 循环中捕获的变量#
下一个:深度克隆对象
评论