【问题标题】:Background Task sometimes able to update UI?后台任务有时能够更新 UI?
【发布时间】:2015-08-07 21:11:25
【问题描述】:

我刚刚回答了一个关于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,是否有某种优化?

【问题讨论】:

  • 如果这是真的,我也会感到惊讶!

标签: c# multithreading task ui-thread


【解决方案1】:

您没有发布异常堆栈跟踪,但我希望它看起来像这样:

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 属性仅在 IsHandleCreatedtrue 时才被调用。

为了完整起见,如果我们查看IsHandleCreated 的代码,我们会看到以下内容:

public bool IsHandleCreated {
    get { return window.Handle != IntPtr.Zero; }
}

所以,您没有收到异常的原因是,在 Task 执行时,尚未创建窗口句柄,这是可以预料的,因为 Task 在表单的构造函数,即在表单显示之前。

在创建窗口句柄之前,修改属性还不需要 UI 线程的任何工作。因此,在程序开始的这个小时间窗口中,似乎可以从非 UI 线程调用控件实例上的方法,而不会出现“跨线程”异常。但很明显,这个特殊的小时间窗口的存在并没有改变我们应该始终确保从 UI 线程调用控制方法以确保安全这一事实。

为了证明创建窗口句柄的时间是获得(或不)“跨线程”异常的决定因素,请尝试修改您的示例以在开始任务之前强制创建窗口句柄,并注意您现在将如何始终如一地获得预期的异常,即使没有睡眠:

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

如果句柄尚未创建,引用此属性将强制创建句柄。

【讨论】:

    猜你喜欢
    • 2015-07-20
    • 2013-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多