后台任务有时能够更新 UI?
Background Task sometimes able to update UI?
我刚刚回答了 Task
是否可以更新 UI 的问题。当我玩我的代码时,我意识到我在一些事情上不清楚自己。
如果我有一个带有一个控件 txtHello
的 windows 表单,我似乎可以从任务更新 UI,如果我立即这样做的话在 Task.Run
上:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Task.Run(() =>
{
txtHello.Text = "Hello";
});
}
}
但是,如果我 Thread.Sleep
甚至 5 毫秒,就会抛出预期的 CrossThread
错误:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Task.Run(() =>
{
Thread.Sleep(5);
txtHello.Text = "Hello"; //kaboom
});
}
}
我不确定为什么会这样。是否对极短的 运行 Task
进行了某种优化?
您没有 post 异常堆栈跟踪,但我希望它看起来像这样:
System.InvalidOperationException: Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on.
at System.Windows.Forms.Control.get_Handle()
at System.Windows.Forms.Control.set_WindowText(String value)
at System.Windows.Forms.TextBoxBase.set_WindowText(String value)
at System.Windows.Forms.Control.set_Text(String value)
at System.Windows.Forms.TextBoxBase.set_Text(String value)
at System.Windows.Forms.TextBox.set_Text(String value)
at WindowsFormsApplicationcSharp2015.Form1.<.ctor>b__0_0() in D:\test\WindowsFormsApplicationcSharp2015\Form1.cs:line 27
我们可以看到从Control.Handle
getter属性抛出了异常。事实上,如果我们查看 属性 的 source code,正如预期的那样:
public IntPtr Handle {
get {
if (checkForIllegalCrossThreadCalls &&
!inCrossThreadSafeCall &&
InvokeRequired) {
throw new InvalidOperationException(SR.GetString(SR.IllegalCrossThreadCall,
Name));
}
if (!IsHandleCreated)
{
CreateHandle();
}
return HandleInternal;
}
}
有趣的部分是当我们查看调用 Control.Handle
的代码时。在这种情况下,就是 Control.WindowText setter 属性:
set {
if (value == null) value = "";
if (!WindowText.Equals(value)) {
if (IsHandleCreated) {
UnsafeNativeMethods.SetWindowText(new HandleRef(window, Handle), value);
}
else {
if (value.Length == 0) {
text = null;
}
else {
text = value;
}
}
}
}
请注意 Handle
属性 仅在 IsHandleCreated
为 true
时才会调用。
为了完整起见,如果我们查看 IsHandleCreated 的代码,我们会看到以下内容:
public bool IsHandleCreated {
get { return window.Handle != IntPtr.Zero; }
}
所以,你没有得到异常的原因是因为在 Task
执行时,window 句柄还没有创建,这是可以预料的,因为Task
从表单的构造函数开始,即在表单显示之前。
在创建 window 句柄之前,修改 属性 不需要来自 UI 线程的任何工作。因此,在程序开始的这段 window 时间里,似乎可以从非 UI 线程调用控件实例上的方法,而不会出现 "cross thread" 异常.但显然,这个特殊的小时间 window 的存在并没有改变我们应该始终确保从 UI 线程调用控制方法是安全的事实。
为了证明 window 句柄创建的时间是获得(或不获得)"cross thread" 异常的决定因素,请尝试修改您的示例以强制创建 window 在开始任务之前处理,并注意你现在如何始终如一地获得预期的异常,即使没有睡眠:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// Force creation of window handle
var dummy = txtHello.Handle;
Task.Run(() =>
{
txtHello.Text = "Hello"; // kaboom
});
}
}
相关文档:Control.Handle
If the handle has not yet been created, referencing this property will force the handle to be created.
我刚刚回答了 Task
是否可以更新 UI 的问题。当我玩我的代码时,我意识到我在一些事情上不清楚自己。
如果我有一个带有一个控件 txtHello
的 windows 表单,我似乎可以从任务更新 UI,如果我立即这样做的话在 Task.Run
上:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Task.Run(() =>
{
txtHello.Text = "Hello";
});
}
}
但是,如果我 Thread.Sleep
甚至 5 毫秒,就会抛出预期的 CrossThread
错误:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Task.Run(() =>
{
Thread.Sleep(5);
txtHello.Text = "Hello"; //kaboom
});
}
}
我不确定为什么会这样。是否对极短的 运行 Task
进行了某种优化?
您没有 post 异常堆栈跟踪,但我希望它看起来像这样:
System.InvalidOperationException: Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on.
at System.Windows.Forms.Control.get_Handle()
at System.Windows.Forms.Control.set_WindowText(String value)
at System.Windows.Forms.TextBoxBase.set_WindowText(String value)
at System.Windows.Forms.Control.set_Text(String value)
at System.Windows.Forms.TextBoxBase.set_Text(String value)
at System.Windows.Forms.TextBox.set_Text(String value)
at WindowsFormsApplicationcSharp2015.Form1.<.ctor>b__0_0() in D:\test\WindowsFormsApplicationcSharp2015\Form1.cs:line 27
我们可以看到从Control.Handle
getter属性抛出了异常。事实上,如果我们查看 属性 的 source code,正如预期的那样:
public IntPtr Handle {
get {
if (checkForIllegalCrossThreadCalls &&
!inCrossThreadSafeCall &&
InvokeRequired) {
throw new InvalidOperationException(SR.GetString(SR.IllegalCrossThreadCall,
Name));
}
if (!IsHandleCreated)
{
CreateHandle();
}
return HandleInternal;
}
}
有趣的部分是当我们查看调用 Control.Handle
的代码时。在这种情况下,就是 Control.WindowText setter 属性:
set {
if (value == null) value = "";
if (!WindowText.Equals(value)) {
if (IsHandleCreated) {
UnsafeNativeMethods.SetWindowText(new HandleRef(window, Handle), value);
}
else {
if (value.Length == 0) {
text = null;
}
else {
text = value;
}
}
}
}
请注意 Handle
属性 仅在 IsHandleCreated
为 true
时才会调用。
为了完整起见,如果我们查看 IsHandleCreated 的代码,我们会看到以下内容:
public bool IsHandleCreated {
get { return window.Handle != IntPtr.Zero; }
}
所以,你没有得到异常的原因是因为在 Task
执行时,window 句柄还没有创建,这是可以预料的,因为Task
从表单的构造函数开始,即在表单显示之前。
在创建 window 句柄之前,修改 属性 不需要来自 UI 线程的任何工作。因此,在程序开始的这段 window 时间里,似乎可以从非 UI 线程调用控件实例上的方法,而不会出现 "cross thread" 异常.但显然,这个特殊的小时间 window 的存在并没有改变我们应该始终确保从 UI 线程调用控制方法是安全的事实。
为了证明 window 句柄创建的时间是获得(或不获得)"cross thread" 异常的决定因素,请尝试修改您的示例以强制创建 window 在开始任务之前处理,并注意你现在如何始终如一地获得预期的异常,即使没有睡眠:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
// Force creation of window handle
var dummy = txtHello.Handle;
Task.Run(() =>
{
txtHello.Text = "Hello"; // kaboom
});
}
}
相关文档:Control.Handle
If the handle has not yet been created, referencing this property will force the handle to be created.