using System;
using System.Windows.Forms;
using System.Threading;
namespace Test
{
public partial class UIThread : Form
{
Worker worker;
Thread workerThread;
public UIThread()
{
InitializeComponent();
worker = new Worker();
worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
workerThread = new Thread(new ThreadStart(worker.StartWork));
workerThread.Start();
}
private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
{
// Cross thread - so you don't get the cross-threading exception
if (this.InvokeRequired)
{
this.BeginInvoke((MethodInvoker)delegate
{
OnWorkerProgressChanged(sender, e);
});
return;
}
// Change control
this.label1.Text = e.Progress;
}
}
public class Worker
{
public event EventHandler<ProgressChangedArgs> ProgressChanged;
protected void OnProgressChanged(ProgressChangedArgs e)
{
if(ProgressChanged!=null)
{
ProgressChanged(this,e);
}
}
public void StartWork()
{
Thread.Sleep(100);
OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
Thread.Sleep(100);
}
}
public class ProgressChangedArgs : EventArgs
{
public string Progress {get;private set;}
public ProgressChangedArgs(string progress)
{
Progress = progress;
}
}
}
// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();
...
// In some non-UI Thread
// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);
...
void UpdateGUI(object userData)
{
// Update your GUI controls here
}
线程代码经常出错并且总是难以测试 . 您无需编写线程代码来从后台任务更新用户界面 . 只需使用BackgroundWorker类来运行任务及其ReportProgress方法即可更新用户界面 . 通常,您只报告完成百分比,但是's another overload that includes a state object. Here'是一个仅报告字符串对象的示例:
using System;
using System.Windows.Forms;
public static class ControlExtensions
{
/// <summary>
/// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
/// </summary>
/// <param name="control"></param>
/// <param name="code"></param>
public static void UIThread(this Control @this, Action code)
{
if (@this.InvokeRequired)
{
@this.BeginInvoke(code);
}
else
{
code.Invoke();
}
}
}
// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
// Running on the UI thread
form.Label.Text = newText;
});
// Back on the worker thread
Main Code (put this inside of your form's class code):
#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
var _with1 = diagwindow;
if (_with1.InvokeRequired) {
_with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
} else {
UpdateDiag(whatmessage);
}
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
var _with2 = diagwindow;
_with2.appendtext(whatmessage);
_with2.SelectionStart = _with2.Text.Length;
_with2.ScrollToCaret();
}
#endregion
这个类似于上面使用.NET Framework 3.0的解决方案,但它解决了 compile-time safety support 的问题 .
public static class ControlExtension
{
delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);
public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
{
if (source.InvokeRequired)
{
var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
source.Invoke(del, new object[]{ source, selector, value});
}
else
{
var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
propInfo.SetValue(source, value, null);
}
}
}
private MyForm _gui;
public void StartToDoThings()
{
_gui = new MyForm();
Thread thread = new Thread(SomeDelegate);
thread.Start();
_gui.ShowDialog();
}
委托可以更新GUI上的Label:
private void SomeDelegate()
{
// Operations that can take a variable amount of time, even no time
//... then you update the GUI
if(_gui.InvokeRequired)
_gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
else
_gui.Label1.Text = "Done!";
}
如果标签更新"take less time"之前的操作(读取并将其解释为简化),则可能导致 InvalidOperationException ,而不是GUI线程创建 Form 的 Handle 所花费的时间 . 这发生在 ShowDialog() 方法中 .
您还应该像这样检查 Handle :
private void SomeDelegate()
{
// Operations that can take a variable amount of time, even no time
//... then you update the GUI
if(_gui.IsHandleCreated) // <---- ADDED
if(_gui.InvokeRequired)
_gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
else
_gui.Label1.Text = "Done!";
}
class SecondThreadConcern
{
public static void LongWork(IProgress<string> progress)
{
// Perform a long running work...
for (var i = 0; i < 10; i++)
{
Task.Delay(500).Wait();
progress.Report(i.ToString());
}
}
}
private async void Button_Click(object sender, EventArgs e)
{
button.Enabled = false;
try
{
var progress = new Progress<string>(s => button.Text = s);
await Task.Run(() => SecondThreadConcern.FailingWork(progress));
button.Text = "Completed";
}
catch(Exception exception)
{
button.Text = "Failed: " + exception.Message;
}
button.Enabled = true;
}
class SecondThreadConcern
{
public static void FailingWork(IProgress<string> progress)
{
progress.Report("I will fail in...");
Task.Delay(500).Wait();
for (var i = 0; i < 3; i++)
{
progress.Report((3 - i).ToString());
Task.Delay(500).Wait();
}
throw new Exception("Oops...");
}
}
20
简单的解决方案是使用 Control.Invoke .
void DoSomething()
{
if (InvokeRequired) {
Invoke(new MethodInvoker(updateGUI));
} else {
// Do Something
updateGUI();
}
}
void updateGUI() {
// update gui here
}
14
这在Ian Kemp解决方案的C#3.0变体中:
public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
var memberExpression = property.Body as MemberExpression;
if (memberExpression == null)
throw new ArgumentException("The 'property' expression must specify a property on the control.");
var propertyInfo = memberExpression.Member as PropertyInfo;
if (propertyInfo == null)
throw new ArgumentException("The 'property' expression must specify a property on the control.");
if (control.InvokeRequired)
control.Invoke(
(Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
new object[] { control, property, value }
);
else
propertyInfo.SetValue(control, value, null);
}
我注意到如果您的代码执行 before the window handle of the control has been created (例如在显示表单之前), Invoke 会抛出异常 . 所以我建议在调用 Invoke 或 BeginInvoke 之前始终检查 InvokeRequired .
如果您使用的是.NET 3.0或更高版本,则可以将上述方法重写为 Control 类的扩展方法,这样可以简化对以下内容的调用:
myLabel.SetPropertyThreadSafe("Text", status);
UPDATE 05/10/2010:
对于.NET 3.0,您应该使用以下代码:
private delegate void SetPropertyThreadSafeDelegate<TResult>(
Control @this,
Expression<Func<TResult>> property,
TResult value);
public static void SetPropertyThreadSafe<TResult>(
this Control @this,
Expression<Func<TResult>> property,
TResult value)
{
var propertyInfo = (property.Body as MemberExpression).Member
as PropertyInfo;
if (propertyInfo == null ||
!@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) ||
@this.GetType().GetProperty(
propertyInfo.Name,
propertyInfo.PropertyType) == null)
{
throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
}
if (@this.InvokeRequired)
{
@this.Invoke(new SetPropertyThreadSafeDelegate<TResult>
(SetPropertyThreadSafe),
new object[] { @this, property, value });
}
else
{
@this.GetType().InvokeMember(
propertyInfo.Name,
BindingFlags.SetProperty,
null,
@this,
new object[] { value });
}
}
它使用LINQ和lambda表达式来实现更清晰,更简单和更安全的语法:
myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile
30 回答
我认为最简单的方法:
当您在UI线程中时,您可以询问它的同步上下文任务调度程序 . 它会给你一个TaskScheduler来安排UI线程上的所有内容 .
然后,您可以链接您的任务,以便在结果准备好后,另一个任务(在UI线程上安排)选择它并将其分配给标签 .
这适用于preferred way of writing concurrent code now的任务(而不是线程) .
这是你应该这样做的经典方式:
您的工作线程有一个事件 . 您的UI线程从另一个线程开始执行工作并挂接该工作事件,以便您可以显示工作线程的状态 .
然后在UI中,您需要跨线程来更改实际控件...如标签或进度条 .
您可以使用已存在的委托
Action
:以前的答案中没有必要的Invoke内容 .
您需要查看WindowsFormsSynchronizationContext:
请注意
BeginInvoke()
优先于Invoke()
,因为它不太可能导致死锁(但是,仅在将文本分配给标签时这不是问题):使用
Invoke()
时,您正在等待返回的方法 . 现在,你可能需要在调用的代码中做一些需要等待线程的东西,如果它隐藏在你正在调用的某些函数中,这可能不会立即明显,这本身可能通过事件处理程序间接发生 . 所以你会等待线程,线程会等你,你就陷入僵局 .这实际上导致我们发布的一些软件挂起 . 通过用
BeginInvoke()
替换Invoke()
很容易解决 . 除非您需要同步操作(如果需要返回值),否则请使用BeginInvoke()
.线程代码经常出错并且总是难以测试 . 您无需编写线程代码来从后台任务更新用户界面 . 只需使用BackgroundWorker类来运行任务及其ReportProgress方法即可更新用户界面 . 通常,您只报告完成百分比,但是's another overload that includes a state object. Here'是一个仅报告字符串对象的示例:
如果您总是想要更新相同的字段,那就没问题 . 如果要进行更复杂的更新,可以定义一个类来表示UI状态并将其传递给ReportProgress方法 .
最后一件事,一定要设置
WorkerReportsProgress
标志,否则ReportProgress
方法将被完全忽略 .只需使用以下内容:
.NET 3.5的Fire and forget扩展方法
这可以使用以下代码行调用:
由于场景的微不足道,我实际上会对状态进行UI线程轮询 . 我想你会发现它可以很优雅 .
该方法避免了使用
ISynchronizeInvoke.Invoke
和ISynchronizeInvoke.BeginInvoke
方法时所需的编组操作 . 使用编组技术没有任何问题,但您需要注意几个警告 .确保不要过于频繁地调用
BeginInvoke
,否则可能会超出消息泵 .在工作线程上调用
Invoke
是一个阻塞调用 . 它将暂时停止该线程中正在进行的工作 .我在这个答案中提出的策略颠倒了线程的通信角色 . UI线程轮询它而不是工作线程推送数据 . 这是许多场景中使用的常见模式 . 由于你想要做的只是显示工作线程的进度信息,我想你会发现这个解决方案是编组解决方案的一个很好的替代方案 . 它具有以下优点 .
UI和工作线程保持松散耦合,而不是紧密耦合它们的
Control.Invoke
或Control.BeginInvoke
方法 .UI线程不会妨碍工作线程的进度 .
工作线程无法控制UI线程花费更新的时间 .
UI和工作线程执行操作的时间间隔可以保持独立 .
工作线程不能超出UI线程的消息泵 .
UI线程决定UI更新的时间和频率 .
您必须使用invoke和delegate
simplest 方式是传递给Label.Invoke的匿名方法:
请注意
Invoke
阻止执行直到它完成 - 这是同步代码 . 这个问题并不是关于异步代码的问题,但是当你想要了解它时,有很多关于编写异步代码的content on Stack Overflow .出于许多目的,它就像这样简单:
“serviceGUI()”是表单(this)中的GUI级方法,可以根据需要更改任意数量的控件 . 从另一个线程调用“updateGUI()” . 可以添加参数来传递值,或者(可能更快)使用带有锁定的类范围变量,如果有可能在访问它们的线程之间发生冲突而导致不稳定 . 使用如果非GUI线程是时间关键的,那么BeginInvoke而不是Invoke(记住Brian Gideon的警告) .
Salvete!搜索了这个问题后,我发现FrankG和Oregon Ghost的答案对我来说是最简单的 . 现在我Visual Basic中的代码并通过转换器运行此代码段;所以我不确定结果如何 .
我有一个名为
form_Diagnostics,
的对话框,其中有一个名为updateDiagWindow,
的richtext框,我将其用作一种日志记录显示 . 我需要能够从所有线程更新其文本 . 额外的线条允许窗口自动滚动到最新的线条 .所以,我现在可以用你认为无需任何线程的方式在整个程序的任何地方用一行更新显示:
Main Code (put this inside of your form's class code):
绝大多数答案使用
Control.Invoke
,这是一个race condition waiting to happen . 例如,考虑接受的答案:如果用户在调用
this.Invoke
之前关闭表单(记住,this
是Form
对象),则可能会触发ObjectDisposedException
.解决方案是使用
SynchronizationContext
,特别是SynchronizationContext.Current
作为hamilton.danielb建议(其他答案依赖于特定的SynchronizationContext
实现,这是完全没有必要的) . 我会略微修改他的代码以使用SynchronizationContext.Post
而不是SynchronizationContext.Send
(因为通常不需要工作线程等待):请注意,在.NET 4.0及更高版本中,您应该使用异步操作的任务 . 有关基于任务的等效方法,请参阅n-san's answer(使用
TaskScheduler.FromCurrentSynchronizationContext
) .最后,在.NET 4.5及更高版本中,您还可以使用
Progress<T>
(基本上在创建时捕获SynchronizationContext.Current
),如Ryszard Dżegan's所示,用于长时间运行的操作需要在仍然工作的情况下运行UI代码的情况 .这个类似于上面使用.NET Framework 3.0的解决方案,但它解决了 compile-time safety support 的问题 .
使用:
如果用户传递了错误的数据类型,编译器将失败 .
.NET 4的Marc Gravell's simplest solution的变化:
或者使用Action委托代替:
请看这里比较两者:MethodInvoker vs Action for Control.BeginInvoke
尝试使用此刷新标签
我的版本是插入 one line 的递归"mantra":
没有参数:
对于具有参数的函数:
THAT is IT .
Some argumentation :通常,在一行中的
if ()
语句之后放置{}会导致代码可读性不佳 . 但在这种情况下,它是常规的"mantra" . 如果此方法在项目中保持一致,则不会破坏代码可读性 . 它可以避免您的代码乱扔垃圾(一行代码而不是五行) .如你所见
if(InvokeRequired) {something long}
,你只知道"this function is safe to call from another thread" .创建一个类变量:
在创建UI的构造函数中设置它:
如果要更新标签:
我只是阅读了答案,这似乎是一个非常热门的话题 . 我目前正在使用.NET 3.5 SP1和Windows Forms .
在之前的答案中大量描述的众所周知的公式使用 InvokeRequired 属性涵盖了大多数情况,但不包括整个池 .
如果尚未创建 Handle 怎么办?
InvokeRequired 属性,如here (Control.InvokeRequired Property reference to MSDN)所述,如果调用是从不是GUI线程的线程调用,则返回true;如果调用是从GUI线程调用,则返回false,或者 Handle 尚未创建 .
如果您希望另一个线程显示和更新模式表单,则可能会遇到异常 . 因为您希望以模态方式显示该表单,所以您可以执行以下操作:
委托可以更新GUI上的Label:
如果标签更新"take less time"之前的操作(读取并将其解释为简化),则可能导致 InvalidOperationException ,而不是GUI线程创建 Form 的 Handle 所花费的时间 . 这发生在 ShowDialog() 方法中 .
您还应该像这样检查 Handle :
如果尚未创建 Handle ,您可以处理要执行的操作:您可以忽略GUI更新(如上面的代码所示)或者您可以等待(风险更大) . 这应该回答这个问题 .
可选的东西:我个人想出了以下代码:
我提供的表单由另一个具有此 ThreadSafeGuiCommand 实例的线程更新,我定义了更新GUI(在我的表单中)的方法,如下所示:
通过这种方式,我非常确定我会更新任何线程进行调用的GUI,可选择等待明确定义的时间(超时) .
您必须确保更新发生在正确的线程上; UI线程 .
为此,您必须调用事件处理程序而不是直接调用它 .
您可以通过以下方式举办活动来做到这一点:
(代码在这里打印出来,所以我没有检查正确的语法等,但它应该让你去 . )
请注意,上面的代码不适用于WPF项目,因为WPF控件不实现
ISynchronizeInvoke
接口 .为了确保上面的代码适用于Windows窗体和WPF以及所有其他平台,您可以查看
AsyncOperation
,AsyncOperationManager
和SynchronizationContext
类 .为了以这种方式轻松地引发事件,我创建了一个扩展方法,它允许我通过调用简化来简化事件:
当然,您也可以使用BackGroundWorker类,它将为您抽象出这个问题 .
您需要在GUI线程上调用该方法 . 您可以通过调用Control.Invoke来实现 .
例如:
例如,访问当前线程以外的控件:
那里
lblThreshold
是一个Label,而Speed_Threshold
是一个全局变量 .处理长期工作
从.NET 4.5 and C# 5.0开始,你应该使用Task-based Asynchronous Pattern (TAP)和async - await关键字in all areas(包括GUI):
而不是Asynchronous Programming Model (APM)和Event-based Asynchronous Pattern (EAP)(后者包括BackgroundWorker Class) .
然后,推荐的新开发解决方案是:
请注意以下事项:
以顺序方式编写的简短而干净的代码,没有回调和显式线程 .
Task而不是Thread .
async关键字,允许使用await,这反过来又阻止事件处理程序达到完成状态,直到任务完成,同时不阻止UI线程 .
Progress类(请参阅IProgress Interface),它支持Separation of Concerns (SoC)设计原则,不需要显式调度程序和调用 . 它使用来自其创建位置的当前SynchronizationContext(此处为UI线程) .
TaskCreationOptions.LongRunning提示不将任务排入ThreadPool .
有关更详细的示例,请参阅:The Future of C#: Good things come to those who 'await' by Joseph Albahari .
另见UI Threading Model概念 .
处理异常
以下代码段是如何处理异常和切换按钮的
Enabled
属性以防止在后台执行期间多次单击的示例 .简单的解决方案是使用
Control.Invoke
.这在Ian Kemp解决方案的C#3.0变体中:
你这样称呼它:
它为"as MemberExpression"的结果添加了空值检查 .
它改善了静态类型安全性 .
否则,原始是一个非常好的解决方案 .
我想添加警告,因为我注意到一些简单的解决方案省略了
InvokeRequired
检查 .我注意到如果您的代码执行 before the window handle of the control has been created (例如在显示表单之前),
Invoke
会抛出异常 . 所以我建议在调用Invoke
或BeginInvoke
之前始终检查InvokeRequired
.当我遇到同样的问题时,我向谷歌寻求帮助,但不是给我一个简单的解决方案,而是通过给出
MethodInvoker
和blah blah blah的例子让我更加困惑 . 所以我决定自己解决它 . 这是我的解决方案:像这样委托:
您可以在这样的新线程中调用此函数
不要与
Thread(() => .....)
混淆 . 我在线程上工作时使用匿名函数或lambda表达式 . 要减少代码行,你也可以使用ThreadStart(..)
方法,我不应该在这里解释 .对于.NET 2.0,这里有一些我编写的代码,它完全符合您的要求,适用于
Control
上的任何属性:这样叫:
如果您使用的是.NET 3.0或更高版本,则可以将上述方法重写为
Control
类的扩展方法,这样可以简化对以下内容的调用:UPDATE 05/10/2010:
对于.NET 3.0,您应该使用以下代码:
它使用LINQ和lambda表达式来实现更清晰,更简单和更安全的语法:
现在不仅在编译时检查属性名称,属性的类型也是如此,因此不可能(例如)将字符串值赋给布尔属性,从而导致运行时异常 .
不幸的是,这并不能阻止任何人做愚蠢的事情,例如传递另一个
Control
的属性和值,所以以下内容将很乐意编译:因此,我添加了运行时检查,以确保传入的属性确实属于调用方法的
Control
. 不完美,但仍然比.NET 2.0版本好很多 .如果有人对如何为编译时安全性改进此代码有任何进一步的建议,请评论!