What is an effective design pattern for 'fan-in' from multiple task threads into a single UI thread?

softwareengineering.stackexchange https://softwareengineering.stackexchange.com/questions/357222

  •  19-01-2021
  •  | 
  •  

Question

In many applications the 'UI thread' is special and framework UI updates must be dispatched from this thread.

However, in order to avoid locking the UI slow tasks are executed in background threads and the results propagated back to the UI thread to be displayed.

Different frameworks solve this in slightly different ways; for example:

  • In WPF the Application.Current.Dispatcher can be used to spawn a task that runs in the UI thread.

  • On android, you can use runOnUiThread to push an action onto the event queue for the UI thread.

  • In QT you bind an event handler using their signal/slot system to receive notifications from the background thread and update using the UI thread event handlers.

  • In iOS you use something like dispatch_async(dispatch_get_main_queue()... to trigger a task on the UI event queue.

All slightly different, but in general the pattern is clear: the background thread creates a Task which it then schedules to be executed on some TaskQueue which the UI thread periodically services in a synchronous manner.

However, I'm not convinced this is actually best practice.

Certainly for few, large background tasks it is reasonable.

...but when you have a large fan-out into sub-threads, and a one-to-one delegation of task-complete -> UI task, this simply smashes the UI with lots of small update tasks, defeating the purpose of moving work into backgrounds threads in the first place.

It seems much more beneficial to have an application level aggregation layer that combines and filters UI updates and only feeds the necessary ones through to the actual UI thread; in many ways this is effectively what the 'Virtual DOM' in react does (but without threads).

So, there's the use case.

Here's the question:

  • What's an effective pattern to write such a thread aggregation layer?

(...and notice that because this is a data-only operation, there is no requirement that this layer is served by a single thread like the UI thread event loop; it may in fact be optimal to service in-coming updates via a thread pool)

Était-ce utile?

La solution

In general, it's best to start writing your application naively since in most cases your UI will remain responsive and it's easier to maintain. However, I have run into situations where the UI gets bogged down with 1,000s of "tasks" in the Queue--most of which are duplicates. It happens often enough in near real-time applications where you are processing one or more streams of data and visualizing them on screen.

I worked on an application like that, and our choice was to keep the data stream separate from the UI. This required some preparation:

  • All data objects needed to either be immutable or not notify the UI that it changed
  • All updates were done on a clock

Essentially the background tasks were free to update the data, adding and removing elements, etc. Once a second the UI looked at the data and updated itself. Since the data was not thread-bound, this didn't cause any problems with synchronization, etc.

The update threshold really depends on your user expectations. For us, we had our sample periods be our limiting factor, so there wasn't any point in updating more often than once a second. Others may have more frequent updates with more localized feedback.

This was the simplest, and most predictable way we could find to keep data and UI in sync without all the micro-tasks that you can sometimes get when you use Dispatcher.BeginInvoke and similar constructs.

Autres conseils

You're right, this is somewhat problematic, that's why it's already solved by the UI framework.

There is usually a way to post partial results to the UI thread. In Swing, that's done via SwingWorker, in Android - AsyncTask. When these are called on the UI thread, all previous partial results are collected and processed by the UI thread at the same time. If there is any way to save time/space by collating these partials instead of redoing the work - that's the time to do it. Easiest is to take the latest result and discard all previous ones (like in a progress bar), essentially meaning that the UI thread may stagger, but doesn't lag.

How this collection is handled - by checking for a previous partial when submitting a new one, or when the UI thread is ready to start processing is really not something you should tinker with unless you really know it's a problem. But of course you can have a look at the library code and find out what the various library designers thought was best.

Licencié sous: CC-BY-SA avec attribution
scroll top