microsoft / Windows-task-snippets

Reusable code snippets for the Universal Windows Platform.
MIT License
306 stars 66 forks source link

Question on bg thread to UI thread #6

Closed quincycs closed 7 years ago

quincycs commented 7 years ago

UI-thread-access-from-background-thread.md

How would you do this if you have a multi window application?

oldnewthing commented 7 years ago

You use the dispatcher for the window whose UI thread you want to dispatch to. I'll make some changes to the snippet to make it clearer. Thanks for the report.

quincycs commented 7 years ago

@oldnewthing also how do you know what UI thread you need to dispatch to? E.g. you use a bg thread, and then you want to notify the UI about a change. How do keep state so that you can map your bg thread to the correct UI thread?

KarlErickson commented 7 years ago

You might also consider using DispatcherHelper, which is based on this snippet but adds support for more scenarios. (I will add a link to this from the snippet.)

oldnewthing commented 7 years ago

You use the UI thread that corresponds to the UI you want to update.

quincycs commented 7 years ago

I'm using helper classes similar to DispatcherHelper. DispatcherHelper doesn't help with knowing what is the right dispatcher to use. Either dispatcherHelper gets the main view's dispatcher or you give it a dispatcher.

Can you provide a sample in which you know the right dispatcher to use? Think of the scenario where you have an app that could duplicate itself with multiple windows. E.g. same page type across multiple windows. And you invoke a bg thread. How do you know which dispatcher to call to update the UI.

Maybe its something like this:

OnNavigatedTo(){

var dispatcher = this.Dispatcher;

ThreadPool.RunAsync(async () =>{

     await Task.Delay(1000);//some network call for example.

     dispatcher.RunAsync( () => {
              //some UI update.
      });
});

}
quincycs commented 7 years ago

Honestly its a little frustrating that you have to keep this state yourself. In a big application where your bg thread is going thru multiple classes and methods, and you want to update the UI. You'd have to pass your dispatcher thru all the layers.

quincycs commented 7 years ago

Kinda wish there was a way to add state onto the bg thread, like an ID to know what is the right dispatcher to use for a UI update.

oldnewthing commented 7 years ago

If your code is running inside a DependencyObject (like a Page), it can ask for this.Dispatcher. Otherwise, it has to get a dispatcher from somewhere.

But the operating system isn't psychic. It doesn't know what UI thread your background thread is doing work on behalf of.

quincycs commented 7 years ago

if there was a way to give the bg thread the state what dispatcher it should use, then it would produce more modular code. E.g. some dictionary mapping bg thread ID to view ID.

quincycs commented 7 years ago

It would be very hard for a single window application , to support multiple windows instances if there isn't a way to make this mapping.

E.g. I have a single window application just use DispatcherHelper.RunOnUI everywhere.

Now I make a new window where all pages could be reached in secondary window. Now you got to change all your DispatcherHelper references everywhere. Bless the soul needing to do multiple windows :)

oldnewthing commented 7 years ago

Well, somebody needs to keep track of which UI thread you want to use. Suppose a background thread calls RunOnUI. But there are two views. What algorithm do you propose for the OS to use in order to decide which UI thread the task should run on? Yes, this is what happens when you have more than one thing. Code that assumes there's only one thing stops working.

KarlErickson commented 7 years ago

Personally, I would keep UI-updating code close to the UI (for example, in the code-behind) so it can grab this.Dispatcher. Background code in non-UI classes could raise events that the UI can handle in order to call RunOnUI and get the effect back onto the right UI thread. This is a more modular approach where the background code is UI-agnostic.

Essentially, this is an architectural concern, and there are many different architectural possibilities. Many large-scale apps use MVVM frameworks that could provide some built-in solutions, for example. There is no single right way to do this, and it's trickier if you are starting from an existing code base that you don't want to have to re-architect.

quincycs commented 7 years ago

@oldnewthing i think the algorithm is pretty simple. Pick the dispatcher that started the background thread in the first place. And make a convention in the app that you should only make a background thread from a UI thread.

@KarlErickson I'd like to know any framework or any library that addresses multi window UWP concerns. I don't think any exists. Yeah its more modular to have events work like that, but you still got to tie all your events together and make sure you release them properly.

Thanks guys for sticking with me in this issue. I realize this is pretty off topic, but this discussion I'm sure will be helpful to someone someday.

KarlErickson commented 7 years ago

I don't know of any frameworks offhand that have support for multiple windows like that, but many of them have messaging systems that provide better decoupling for events.

oldnewthing commented 7 years ago

Having a rule that only UI threads can create background threads means that we would have to deprecate Windows.Threading.ThreadPool. Also, the system internally creates background threads triggered by external events, not by a UI thread. E.g. "the accelerometer detected motion" or "A network response was received."

quincycs commented 7 years ago

thx.

@oldnewthing RE: threadPool. Not a suggestion for the platform to change, but rather an app developer to choose to make this rule in their code. For any of these external bg thread events, you could immediately invoke MainView's dispatcher or invoke all Views' dispatchers. Then as @KarlErickson is suggesting, (use a message subscription pattern) publish an event that listeners can handle. I implemented my own flavor of https://msdn.microsoft.com/en-us/library/ff921122.aspx . I'm sure the UWP community lib has something similar.

quincycs commented 7 years ago

I think these external bg thread events aren't as super common as what the dispatcher is usually used for.

quincycs commented 7 years ago

Ok, what do you think about this... use the following IDs so that the background thread knows what dispatcher to use later.

taskID = TaskScheduler.Current.Id uiID = ApplicationView.GetForCurrentView().Id

Anytime a view is created, initialize map[taskID] = uiID; Anytime a bg thread is created, initialize map[taskID] = uiID;

Anytime you need to get a dispatcher whether on UI thread or on BG thread, look at map[taskID] , then go thru the views to find the right view.

currentView = CoreApplication.Views.FirstOrDefault(x => x.CoreWindow != null && viewId == ApplicationView.GetApplicationViewIdForWindow(x.CoreWindow));

then

currentView.Dispatcher

oldnewthing commented 7 years ago

That doesn't work because a single TaskScheduler can run multiple tasks. (Indeed, in the default configuration, there is only one TaskScheduler that runs all tasks for the entire process.) Each task may be running on behalf of a different UI thread. You could use Task.CurrentId, but that won't help if you call it from a background thread not inside a task. (E.g., the event handler for a WinRT event.)

For event handlers, they need to be told which UI thread they are working on behalf of, because they can be invoked from any thread (and probably not as part of a task). The usual way of doing this is attaching a Dispatcher to the object that houses the event handler.

quincycs commented 7 years ago

oh duh, TaskScheduler.Current.Id is always 1.

quincycs commented 7 years ago

hm is there a way to set some value on the "thread" or "task" or whatever level so that we could get the value later? I'm thinking the value would just be the viewId. The problem is how to store basically this thread-level data.

oldnewthing commented 7 years ago

If the event originates from the OS, then there is no task, and you don't control the thread. So even if you could put the viewId on the thread, that wouldn't help because the OS isn't using your thread.

quincycs commented 7 years ago

Yeah, but I think if it's an event, then its pretty easy to route that to the UI that subscribed to it or route it to all windows to run a UI update.

What about the normal case - if the app goes thru threadpool to create bg thread. Is there a way to add some contextual data on that for it to know what viewId to use later?

oldnewthing commented 7 years ago

Use lambda capture. Capture into the lambda the dispatcher or the viewId or whatever other information you need, and then consume the data in the lambda body.

quincycs commented 7 years ago

yeah, then pass around that data via method parameters wherever you continue to need it.

quincycs commented 7 years ago

Let's say you have an app with a resourcedictionary full of templates & brushes that is meant to be used across the app.

If you have multiple windows, is that wrong to use a central resource dictionary? B/c resource dictionary has a dispatcher on it, so my concern is that making another window use it will have undefined effects.

I tried it and sometimes I see the UI theme be wrong for a secondary window.


Just want to say thank you to @oldnewthing and @KarlErickson for brainstorming with me!

oldnewthing commented 7 years ago

These aren't really questions about the task snippet. You can open a support case with our Developer Support team who can further help with these questions.  

  1. Visit http://aka.ms/storesupport and sign-in with your Microsoft account
  2. Under the “App Development” section choose "Windows 10 universal app development", then choose the appropriate problem type and category to get engaged through a formal support channel.
quincycs commented 7 years ago

thank you