开发者

Why is the pattern to update UI from a different thread not built into the .NET framework?

I know "why is my this framework like/not like xyz?" questions are a bit dangerous but I want to see what I'm missing.

In WinForms, you can't update the UI from another thread. Most people use 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
}

and more clever still is 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);
}

Regardless of which is used though, everyone has to write boilerplate code to handle this incredibly common problem. Why then, has the .NET Framework not been updated to do this for us? Is it that this area of the codebase is frozen? Is it a concern that it would break backwards compatibility? Is it a concern about the confusion when some code works one way in version N and a different way in version N+1?


I thought it might be interesting to mention why it is that there's a UI thread in the first place. It is to lower the cost of the production of UI components while increasing their correctness and robustness.

The basic problem of thread safety is that a non-atomic update of private state can be observed to be half-finished on a reading thread if the writing thread is halfway done when the read happens.

To achieve thread safety there are a number of things you can do.

1) Explicitly lock all reads and writes. Pros: maximally flexible; everything works on any thread. Cons: Maximally painful; everything has to be locked all the time. Locks can be contended, which makes them slow. It is very easy to write deadlocks. It is very easy to write code that handles re-entrancy poorly. And so on.

2) Allow reads and writes only on the thread that created the object. You can have multiple objects on multiple threads, but once an object is used on a thread, that's the only thread that can use it. Therefore there will be no reads and writes on different threads simultaneously, so you don't need to lock anything. This is the "apartment" model, and it is the model that the vast majority of UI components are built to expect. The only state that needs to be locked is state shared by multiple instances on different threads, and that's pretty easy to do.

3) Allow reads and writes only on the owning thread, but allow one thread to explicitly hand off ownership to another when there are no reads and writes in progress. This is the "rental" model, and it is the model used by Active Server Pages to recycle script engines.

Since the vast majority of UI components are written to work in the apartment model, and it is painful and difficult to make all those components free-threaded, you're stuck with having to do all UI work on the UI thread.


It is built-in, the BackgroundWorker class implements it automatically. Its event handlers run on the UI thread, assuming it is properly created.

Taking a more cynical approach to this: it is an anti-pattern. At least in my code, it is extremely rare that the same method run on both the UI thread and some worker thread. It just doesn't make sense to test InvokeRequired, I know it is always is true since I wrote the code that would call it intentionally as code that runs in a separate thread.

With all the necessary plumbing that's required to make such code safe and interop correctly. Using the lock statement and Manual/AutoResetEvents to signal between the threads. And if InvokeRequired would be false then I know I have a bug in that code. Because invoking to the UI thread when the UI components are either not yet created or disposed is Very Bad. It belongs in a Debug.Assert() call, at best.


I think that the problem is that this construct would probably need to be built into every property of all UI components in the framework. This would also need to be done by third party developers making such components.

Another option might be that the compilers added the construct around accesses to UI components, but this would add complexity to the compilers instead. As I understand it, for a feature to make way into the compiler it should

  • play well will all other existing features in the compiler
  • have an implementation cost that makes it worth the while in relation to the problem it solves

In this particular case the compiler would also need to have a way of determining whether a type in the code is a type that needs the synchronization construct around it or not.

Of course all of these are speculations, but I could imagine that this sort of reasoning lies behind the decision.


A cleaner pattern in.NET 4 is using TPL and continuations. See Link

Use

var ui = TaskScheduler.FromCurrentSynchronizationContext();

and now you can easily request that continuations run on the UI thread.


If I understand your question correctly, you'd want the framework (or compiler or some other piece of technology) to include Invoke/BeginInvoke/EndInvoke around all public members of a UI object, to make it thread safe. The problem is: That alone wouldn't make your code thread safe. You'd still have to use BeginInvoke and other syncronization mechanisms very often. (See this great article on thread safety on Eric Lippert's Blog)

Imagine you write code like

if (myListBox.SelectedItem != null) 
{
    ...
    myLabel.Text = myListBox.SelectedItem.Text;
    ...
}

If the framework or compiler wrapped every access to SelectedItem and the call to Delete in a BeginInvoke/Invoke call, this wouldn't be thread safe. There's a potential race condition if SelectedItem is non-null when the if-clause is evaluated, but another thread sets it to null before the then-block is completed. Probably the whole if-then-else-clause should be wrapped in a BeginInvoke-call, but how should the compiler know this?

Now you could say "but that's true for all shared mutable objects, I'll just add locks". But that is quite dangerous. Imagine you did something like:

// 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
}

This will deadlock: Method A is called in a background thread. It acquires the lock, calls Invoke. Invoke waits for a response from the UI thread's message-queue. At the same time, method B is executed in the main thread (e.g. in a Button.Click-Handler). The other thread holds myListBoxLock, so it can't enter the lock - now both threads wait on each other, neither can make any progress.

Finding and avoiding threading-bugs like these is hard enough as it is, but at least now you can see that you're calling Invoke and that it's a blocking call. If any property could silently block, bugs like these would be far harder to find.

Moral: Threading is hard. Thread-UI-interaction is even harder, because there is only one shared single message queue. And sadly, neither our compilers nor our frameworks are smart enough to "just make it work right".

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜