为什么从不同线程更新UI的模式没有内置到.NET framework中?

为什么从不同线程更新UI的模式没有内置到.NET framework中?,.net,winforms,multithreading,asynchronous,.net,Winforms,Multithreading,Asynchronous,我知道“为什么我的这个框架像/不像xyz?”这些问题有点危险,但我想看看我遗漏了什么 在WinForms中,无法从其他线程更新UI。大多数人使用: 更聪明的是: 不管使用哪种方法,每个人都必须编写样板代码来处理这个极其常见的问题。那么,为什么.NET框架没有更新来为我们做到这一点呢?是不是代码库的这个区域被冻结了?是否担心它会破坏向后兼容性?当某些代码在版本N中以一种方式工作,而在版本N+1中以另一种方式工作时,这是否会引起混淆?我认为问题在于,这个构造可能需要构建到框架中所有UI组件的每个属性

我知道“为什么我的这个框架像/不像xyz?”这些问题有点危险,但我想看看我遗漏了什么

在WinForms中,无法从其他线程更新UI。大多数人使用:

更聪明的是:


不管使用哪种方法,每个人都必须编写样板代码来处理这个极其常见的问题。那么,为什么.NET框架没有更新来为我们做到这一点呢?是不是代码库的这个区域被冻结了?是否担心它会破坏向后兼容性?当某些代码在版本N中以一种方式工作,而在版本N+1中以另一种方式工作时,这是否会引起混淆?我认为问题在于,这个构造可能需要构建到框架中所有UI组件的每个属性中。这也需要由制造此类组件的第三方开发人员来完成

另一种选择可能是编译器围绕对UI组件的访问添加了构造,但这反而会增加编译器的复杂性。据我所知,要让一个特性进入编译器,它应该

  • 编译器中的所有其他现有功能都将得到很好的发挥
  • 有一个实施成本,使它值得与它所解决的问题相关
在这种特殊情况下,编译器还需要有一种方法来确定代码中的类型是否是需要在其周围使用同步构造的类型

当然,所有这些都是推测,但我可以想象,这种推理背后的决定。

它是内置的,BackgroundWorker类自动实现它。它的事件处理程序在UI线程上运行,假设它已正确创建

对此采取更为愤世嫉俗的态度:这是一种反模式。至少在我的代码中,在UI线程和一些工作线程上运行相同的方法是非常罕见的。测试InvokeRequired是没有意义的,我知道它总是正确的,因为我编写了有意调用它的代码,作为在单独线程中运行的代码


提供了所有必要的管道,以确保此类代码的安全性和正确的互操作性。使用lock语句和手动/自动事件在线程之间发送信号。如果InvokeRequired为false,那么我知道代码中有一个bug。因为在UI组件尚未创建或释放时调用UI线程是非常糟糕的。它最多只属于Debug.Assert()调用。

在.NET 4中,一个更干净的模式是使用TPL和continuations。看

使用


现在,您可以轻松地请求在UI线程上运行continuations。

如果我正确理解您的问题,您可能希望框架(或编译器或其他技术)在UI对象的所有公共成员周围包含Invoke/BeginInvoke/EndInvoke,以使其线程安全。问题是:单凭这一点并不能保证代码的线程安全。您仍然必须经常使用BeginInvoke和其他同步机制。(见此)

假设您编写的代码如下所示

if (myListBox.SelectedItem != null) 
{
    ...
    myLabel.Text = myListBox.SelectedItem.Text;
    ...
}
如果框架或编译器在BeginInvoke/Invoke调用中包装了对
SelectedItem
的每次访问和对
Delete
的调用,那么这将不是线程安全的。如果在计算if子句时,
SelectedItem
为非null,但另一个线程在then块完成之前将其设置为null,则可能存在争用条件。也许整个if-then-else子句应该包装在一个BeginInvoke调用中,但是编译器应该如何知道这一点呢

现在你可以说“但是对于所有共享的可变对象都是这样,我只添加锁”。但这是相当危险的。想象一下你做了如下事情:

// in method A
lock (myListBoxLock)
{
    // do something with myListBox that secretly calls Invoke or EndInvoke
}

// in method B
lock (myListBoxLock)
{
    // do something else with myListBox that secretly calls Invoke or EndInvoke
}
这将导致死锁:在后台线程中调用方法A。它获取锁,调用Invoke。Invoke等待来自UI线程消息队列的响应。同时,方法B在主线程中执行(例如,在按钮单击处理程序中)。另一个线程持有
myListBoxLock
,因此它无法进入锁-现在两个线程都在互相等待,都无法取得任何进展

找到和避免像这样的线程错误已经很难了,但至少现在你可以看到你正在调用Invoke,这是一个阻塞调用。如果有任何属性可以静默地阻止,那么像这样的bug将很难找到


寓意:穿线很难。线程UI交互更加困难,因为只有一个共享的单个消息队列。不幸的是,无论是我们的编译器还是我们的框架,都没有足够的智能来“让它正常工作”。

我想,首先提到为什么会有一个UI线程可能会很有趣。这是为了降低UI组件的生产成本,同时提高它们的正确性和健壮性

线程安全的基本问题是,如果读取发生时写入线程完成了一半,则可以观察到私有状态的非原子更新在读取线程上完成了一半

要实现线程安全,可以做很多事情

1) 显式锁定所有读写操作。优点:最大限度地灵活;任何线程都可以工作。缺点:最大程度的疼痛;所有东西都必须一直锁着。锁可以争用,这会使它们变慢。写死锁很容易。编写处理重入性差的代码非常容易。等等

2) 仅允许在创建对象的线程上进行读取和写入。您可以在多个线程上有多个对象,但一旦在一个线程上使用了一个对象,这就是唯一可以使用它的线程。因此,在不同的线程上不会同时进行读写操作,因此不需要锁定任何内容。这就是“公寓”模型,也是绝大多数UI组件构建所期望的模型。Th
var ui = TaskScheduler.FromCurrentSynchronizationContext();
if (myListBox.SelectedItem != null) 
{
    ...
    myLabel.Text = myListBox.SelectedItem.Text;
    ...
}
// in method A
lock (myListBoxLock)
{
    // do something with myListBox that secretly calls Invoke or EndInvoke
}

// in method B
lock (myListBoxLock)
{
    // do something else with myListBox that secretly calls Invoke or EndInvoke
}