【发布时间】:2011-05-03 11:30:38
【问题描述】:
目前我正在使用 WinForms(在 C# 中),我必须在后台运行应用程序。为此,我使用异步。当我运行应用程序时,它会显示一个异常,例如
“跨线程操作无效:控件''从创建它的线程以外的线程访问。”
我该如何解决这个错误?
【问题讨论】:
标签: c# winforms multithreading
目前我正在使用 WinForms(在 C# 中),我必须在后台运行应用程序。为此,我使用异步。当我运行应用程序时,它会显示一个异常,例如
“跨线程操作无效:控件''从创建它的线程以外的线程访问。”
我该如何解决这个错误?
【问题讨论】:
标签: c# winforms multithreading
当对控件进行方法调用时,如果调用者在与创建控件的线程不同的线程上,则需要使用Control.Invoke 进行调用。这是一个代码示例:
// you can define a delegate with the signature you want
public delegate void UpdateControlsDelegate();
public void SomeMethod()
{
//this method is executed by the background worker
InvokeUpdateControls();
}
public void InvokeUpdateControls()
{
if (this.InvokeRequired)
{
this.Invoke(new UpdateControlsDelegate(UpdateControls));
}
else
{
UpdateControls();
}
}
private void UpdateControls()
{
// update your controls here
}
希望对你有帮助。
【讨论】:
通常,使用 WinForms 执行此类操作的最佳方法是使用 BackgroundWorker,它将在后台线程上运行您的工作,但为您提供了一种将状态报告回 UI 的简洁方式。
在许多日常 .NET 编程中,显式创建线程或调用 .Invoke 表明您没有充分利用该框架(当然,做低级工作有很多正当理由同样,只是人们有时意识到它们不太常见)。
【讨论】:
您需要检查您尝试更新的控件是否需要 Invoke。像这样的:
Action<Control, string> setterCallback = (toSet, text) => toSet.Text = text;
void SetControlText(Control toSet, string text) {
if (this.InvokeRequired) {
this.Invoke(setterCallback, toSet, text);
}
else {
setterCallback(toSet, text);
}
}
【讨论】:
从 Invoke 更新为开始 Invoke
// you can define a delegate with the signature you want
public delegate void UpdateControlsDelegate();
public void SomeMethod()
{
//this method is executed by the background worker
InvokeUpdateControls();
}
public void InvokeUpdateControls()
{
if (this.InvokeRequired)
{
this.BeginInvoke(new UpdateControlsDelegate(UpdateControls));
}
else
{
UpdateControls();
}
}
private void UpdateControls()
{
// update your controls here
}
【讨论】:
InvokeUpdateControls 有点危险。如果需要调用,那么您将异步执行委托,即您将其安排在以后的时间,但如果您在同一个线程上(或尚未创建窗口句柄),则您正在执行它本身。这可能需要也可能不需要,但应该牢记差异。
您可能会发现一种有用的模式是在与 GUI 交互的函数的顶部进行检查,以查看您是否在正确的线程上运行,并在需要时让函数自行调用。像这样:
public delegate void InvocationDelegate();
public void DoGuiStuff(){
if (someControl.InvokeRequired){
someControl.Invoke(InvocationDelegate(DoGuiStuff));
return;
}
//GUI manipulation here
}
使用这种模式 - 如果您在调用该方法时位于正确的线程上,则它不会调用自身,但如果您在不同的线程上,它会调用自身然后返回(因此 GUI 操作逻辑只是永远无论哪种方式都调用一次)。
【讨论】:
BeginInvoke
这是防止跨线程异常的好方法。我在《C#程序员学习指南(MCSD)》一书中读到它
您可以使用 BeginInvoke
BeginInvoke 方法用于从其他线程更改 UI 控件的值。它以线程安全的方式进行。它需要一个代表;它告诉哪个 UI 控件需要更改其值。
private async void button1_Click(object sender, EventArgs e)
{
Task task = Task.Run(() =>
{
this.BeginInvoke(new Action(() =>
{
label1.Text = "Hello";
}));
});
await task;
}
label1.Text 的值应改为“Hello”,不会出现异常,因为它是线程安全操作。
【讨论】:
UI 改变可以通过 Control.Invoke() 方法来完成,这个跨线程异常可以使用下面的代码 sn-p 来解决。
void UpdateWorker()
{
//Here ddUser is the user control
//Action to be performed should be called within { } as like below code
if (this.ddUser.InvokeRequired)
ddUser.Invoke(new MethodInvoker(() => { ddUser.Size = new Size(100, 100); }));
}
【讨论】:
我知道这个话题已有 10 年的历史了,但我想通过 lambda 选择器而不是定义每种类型的 setter 来改进泛型的解决方案
private void SetControlSafety<C, V>(C control, Expression<Func<C, V>> selector, V value)
{
if (this.InvokeRequired)
this.Invoke(MyUtils.GetSetter(selector), control, value);
else
DataCrawlerUtils.GetSetter(selector)(control, value);
}
或静态
public static void SetControlSafety<C, V>(C control, Expression<Func<C, V>> selector, V value) where C : Control
{
if (control.InvokeRequired)
control.Invoke(DataCrawlerUtils.GetSetter(selector), control, value);
else
DataCrawlerUtils.GetSetter(selector)(control, value);
}
已通过 lambda 选择了来自 here 的用于为属性赋值的 GetSetter 方法
public static Action<T, TProperty> GetSetter<T, TProperty>(
Expression<Func<T, TProperty>> pExpression
)
{
var parameter1 = Expression.Parameter(typeof(T));
var parameter2 = Expression.Parameter(typeof(TProperty));
// turning an expression body into a PropertyInfo is common enough
// that it's a good idea to extract this to a reusable method
var member = (MemberExpression)pExpression.Body;
var propertyInfo = (PropertyInfo)member.Member;
// use the PropertyInfo to make a property expression
// for the first parameter (the object)
var property = Expression.Property(parameter1, propertyInfo);
// assignment expression that assigns the second parameter (value) to the property
var assignment = Expression.Assign(property, parameter2);
// then just build the lambda, which takes 2 parameters, and has the assignment
// expression for its body
var setter = Expression.Lambda<Action<T, TProperty>>(
assignment,
parameter1,
parameter2
);
return setter.Compile();
}
那么使用就很简单了
SetControlSafety(txtStatus, x => x.Text, "Loading resources...");
【讨论】: