我知道“为什么我的这个框架喜欢/不喜欢xyz?”问题有点危险,但我想看看我错过了什么。
在WinForms中,您无法从其他线程更新UI。大多数人使用this pattern:
private void EventHandler(object sender, DirtyEventArgs e)
{
if (myControl.InvokeRequired)
myControl.Invoke(new MethodInvoker(MethodToUpdateUI), e);
else
MethodToUpdateUI(e);
}
private void MethodToUpdateUI(object obj)
{
// Update UI
}
更聪明的是this pattern:
public static TResult SafeInvoke(this T isi, Func call) where T : ISynchronizeInvoke
{
if (isi.InvokeRequired) {
IAsyncResult result = isi.BeginInvoke(call, new object[] { isi });
object endResult = isi.EndInvoke(result); return (TResult)endResult;
}
else
return call(isi);
}
public static void SafeInvoke(this T isi, Action call) where T : ISynchronizeInvoke
{
if (isi.InvokeRequired)
isi.BeginInvoke(call, new object[] { isi });
else
call(isi);
}
无论使用哪种方法,每个人都必须编写样板代码来处理这个令人难以置信的常见问题。为什么.NET Framework没有更新为我们这样做呢?这个代码库的区域是冻结的吗?是否会破坏向后兼容性?当一些代码在版本N中以单向方式工作而在版本N + 1中以不同方式工作时,是否会引起混淆?
答案 0 :(得分:14)
我认为提到为什么首先存在UI线程可能会很有趣。它是为了降低UI组件的生产成本,同时提高其正确性和稳健性。
线程安全的基本问题是,如果在读取发生时写入线程已完成一半,则可以在读取线程上观察到私有状态的非原子更新是半完成的。
为了实现线程安全,您可以做很多事情。
1)明确锁定所有读写操作。优点:最大的灵活性;一切都适用于任何线程。缺点:最痛苦;一切都必须一直锁定。锁可以争用,这使它们变慢。编写死锁非常容易。编写处理重新入门的代码非常容易。等等。
2)仅允许在创建对象的线程上进行读写。您可以在多个线程上拥有多个对象,但是一旦在线程上使用了对象,那么这是唯一可以使用它的线程。因此,不同的线程不会同时进行读写操作,因此您无需锁定任何内容。这是“公寓”模型,它是绝大多数UI组件构建的模型。唯一需要锁定的状态是由不同线程上的多个实例共享的状态,这很容易做到。
3)仅允许在拥有线程上进行读写,但是当没有正在进行的读写操作时,允许一个线程明确地将所有权移交给另一个。这是“租用”模型,它是Active Server Pages用于回收脚本引擎的模型。
由于绝大多数UI组件都是在公寓模型中编写的,并且使所有这些组件都是自由线程是很困难和困难的,因此您不得不在UI线程上完成所有UI工作。
答案 1 :(得分:4)
我认为问题在于这个构造可能需要构建到框架中所有UI组件的每个属性中。这也需要由制作此类组件的第三方开发人员来完成。
另一种选择可能是编译器围绕对UI组件的访问添加了构造,但这会增加编译器的复杂性。据我所知,对于一个进入编译器的功能,它应该
在这种特殊情况下,编译器还需要有一种方法来确定代码中的类型是否是需要围绕它的同步构造的类型。
当然所有这些都是猜测,但我可以想象这种推理背后的决定。
答案 2 :(得分:4)
内置了 ,BackgroundWorker类自动实现它。它的事件处理程序在UI线程上运行,假设它已正确创建。
采取更为愤世嫉俗的方法:这是一种反模式。至少在我的代码中,非常很少相同的方法在UI线程和一些工作线程上运行。测试InvokeRequired是没有意义的,我知道它始终是真的,因为我编写的代码将故意称为作为在单独线程中运行的代码。
使用所需的所有必要的管道来使这些代码安全并正确地互操作。使用lock语句和Manual / AutoResetEvents在线程之间发出信号。如果InvokeRequired将 false ,那么我知道我的代码中有一个错误。因为当UI组件尚未创建或处置时调用UI线程非常糟糕。它最多只属于Debug.Assert()调用。
答案 3 :(得分:2)
.NET 4中的清洁模式正在使用TPL和continuation。见http://blogs.msdn.com/b/csharpfaq/archive/2010/06/18/parallel-programming-task-schedulers-and-synchronization-context.aspx
使用
var ui = TaskScheduler.FromCurrentSynchronizationContext();
现在您可以轻松地请求在UI线程上运行延续。
答案 4 :(得分:0)
如果我正确理解您的问题,您希望框架(或编译器或其他一些技术)在UI对象的所有公共成员周围包含Invoke / BeginInvoke / EndInvoke,以使其线程安全。问题是:仅凭这一点不会使您的代码线程安全。您仍然必须经常使用BeginInvoke和其他同步机制。 (见great article on thread safety on Eric Lippert's Blog)
想象一下,您编写了像
这样的代码if (myListBox.SelectedItem != null)
{
...
myLabel.Text = myListBox.SelectedItem.Text;
...
}
如果框架或编译器在BeginInvoke / Invoke调用中包含对SelectedItem
的每次访问以及对Delete
的调用,则此将不是线程安全的。如果在评估if子句时SelectedItem
为非null,则存在潜在的竞争条件,但另一个线程在then-block完成之前将其设置为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在主线程中执行(例如,在Button.Click-Handler中)。另一个线程保持myListBoxLock
,因此它无法进入锁定 - 现在两个线程都相互等待,两者都无法取得任何进展。
找到并避免像这样的线程错误很难实现,但至少现在你可以看到你正在调用Invoke并且它是一个阻塞调用。如果任何属性可以静默阻止,那么像这样的错误远更难找到。
道德:线程很难。 Thread-UI-interaction更难,因为只有一个共享的单个消息队列。遗憾的是,我们的编译器和框架都不够聪明,“只是让它正常工作”。