I love polling! Do I? Yes! Do I? Yes! Do I? Yes! Do I still? Yes! What about now? Yes!
As others have mentioned, it can be incredibly inefficient if you're polling only to get back the same unchanged state over and over. Such is a recipe for burning up CPU cycles and significantly shortening battery life on mobile devices. Of course it's not wasteful if you're getting back a new and meaningful state every single time at a rate no faster than desired.
But the main reason I love polling is because of its simplicity and predictable nature. You can trace through the code and easily see when and where things are going to happen, and in what thread. If, theoretically, we lived in a world where polling was a negligible waste (albeit the reality being far from it), then I believe it would simplify maintaining code a tremendous deal. And that's the benefit of polling and pulling as I see if we could disregard performance, even though we shouldn't in this case.
When I started programming in the DOS era, my little games revolved around polling. I copied some assembly code from a book which I barely understood relating to keyboard interrupts and made it store a buffer of keyboard states, at which point my main loop was always polling. Is the up key down? Nope. Is the up key down? Nope. How about now? Nope. Now? Yes. Okay, move the player.
And while incredibly wasteful, I found that so much easier to reason about compared to these days of multi-tasking and event-driven programming. I knew exactly when and where things would occur at all times and it was easier to keep frame rates stable and predictable without hiccups.
So since then I've always been trying to find a way to get some of the benefits and predictability of that without actually burning up CPU cycles, like using condition variables to notify threads to wake up at which point they can pull the new state, do their thing, and go back to sleep waiting to be notified again.
And somehow I find event queues a lot easier to work with at least than observer patterns, even though they still don't make it so easy to predict where you'll end up going or what will end up happening. They at least centralize the event handling control flow to a few key areas in the system and always handling those events in the same thread instead of bouncing from one function to somewhere completely remote and unexpected all of a sudden outside of a central event handling thread. So the dichotomy doesn't always have to be between observers and polling. Event queues are kind of a middle ground there.
But yeah, somehow I find it so much easier to reason about systems that do things that are analogically closer to the kind of predictable control flows I used to have when I was polling ages ago, while just counteracting the tendency for the work to occur at times when no state changes have occurred. So there's that benefit if you can do it in a way that isn't burning CPU cycles needlessly like with condition variables.
Homogeneous Loops
All right, I got a great comment from Josh Caswell
that pointed out some goofiness in my answer:
"like using condition variables to notify threads to wake up" Sounds
like an event-based/observer arrangement, not polling
Technically the condition variable itself is applying the observer pattern to wake up/notify threads, so to call that "polling" would probably be incredibly misleading. But I find it provides a similar benefit I found as polling from the DOS days (just in terms of control flow and predictability). I'll try to explain it better.
What I found appealing back in those days was that you could look at a section of code or be tracing through it and say, "Okay, this entire section is dedicated to handling keyboard events. Nothing else is going to happen in this section of code. And I know exactly what's going to happen before, and I know exactly what's going to happen after (physics and rendering, e.g.)." The polling of the keyboard states gave you that kind of centralization of control flow as far as handling what should go on in response to this external event. We didn't respond to this external event immediately. We responded to it at our convenience.
When we use a push-based system based on an Observer pattern, we often lose those benefits. A control might get resized which triggers a resize event. When we trace through it, we find we're inside exotic control which does a lot of custom things in its resizing which triggers more events. We end up being completely surprised tracing into all these cascading events as to where we end up in the system. Furthermore, we might find that all of this doesn't even consistently occur in any given thread because thread A might resize a control here while thread B also resizes a control later on. So I always found this very difficult to reason about given how difficult it is to predict where everything happens as well as what will happen.
The event queue is a little bit simpler to me to reason about because it simplifies where all these things happen at least at a thread level. However, many disparate things could be happening. An event queue could contain an eclectic mixture of events to process, and each one could still surprise us as to what cascade of events that occurred, the order in which they were processed, and how we end up bouncing all over the place in the codebase.
What I'm considering "closest" to polling would not use an event queue but would defer a very homogeneous type of processing. A PaintSystem
might be alerted through a condition variable that there's painting work to do to repaint certain grid cells of a window, at which point it does a simple sequential loop through the cells and repaints everything inside it in proper z-order. There might be one level of calling indirection/dynamic dispatch here to trigger the paint events in each widget residing in a cell that needs to be repainted, but that's it -- just one layer of indirect calls. The condition variable uses the observer pattern to alert the PaintSystem
that it has work to do, but it doesn't specify anything more than that, and the PaintSystem
is devoted to one uniform, very homogeneous task at that point. When we're debugging and tracing through the PaintSystem's
code, we know that nothing else will happen except painting.
So it's mostly about getting the system down to where you have these things performing homogeneous loops over data applying a very singular responsibility over it instead of non-homogeneous loops over disparate types of data performing numerous responsibilities as we might get with event queue processing.
We're aiming for this type of thing:
when there's work to do:
for each thing:
apply a very specific and uniform operation to the thing
As opposed to:
when one specific event happens:
do something with relevant thing
in relevant thing's event:
do some more things
in thing1's triggered by thing's event:
do some more things
in thing2's event triggerd by thing's event:
do some more things:
in thing3's event triggered by thing2's event:
do some more things
in thing4's event triggered by thing1's event:
cause a side effect which shouldn't be happening
in this order or from this thread.
And so forth. And it doesn't have to be one thread per task. One thread might apply layouts (resizing/repositioning) logic for GUI controls and repaint them, but it might not handle keyboard or mouse clicks. So you could look at this as just improving the homogeneity of an event queue. But we don't have to use an event queue and interleave resizing and painting functions either. We can do like:
in thread dedicated to layout and painting:
when there's work to do:
for each widget that needs resizing/reposition:
resize/reposition thing to target size/position
mark appropriate grid cells as needing repainting
for each grid cell that needs repainting:
repaint cell
go back to sleep
So the above approach just uses a condition variable to notify the thread when there's work to do, but it doesn't interleave different types of events (resize in one loop, paint in another loop, not a mixture of both) and it doesn't bother to communicate what the work is exactly that needs to be done (the thread "discovers" that upon waking up by looking at system-wide states of the ECS). Each loop it performs is then very homogeneous in nature, making it easy to reason about the order in which everything happens.
I'm not sure what to call this type of approach. I haven't seen other GUI engines do this and it's kind of my own exotic approach to mine. But before when I tried to implement multithreaded GUI frameworks using observers or event queues, I had a tremendous difficulty debugging it and also ran into some obscure race conditions and deadlocks that I wasn't smart enough to fix in a way that made me feel confident about the solution (some people might be able to do this but I'm not smart enough). My first iteration design just called a slot directly through a signal and some slots would then spawn other threads to do async work, and that was the hardest to reason about and I was tripping over race conditions and deadlocks. Second iteration used an event queue and that was a little easier to reason about, but not easy enough for my brain to do it without still running into the obscure deadlock and race conditiion. Third and final iteration used the approach described above, and finally that allowed me to create a multithreaded GUI framework that even a dumb simpleton like me could implement correctly.
Then this type of final multithreaded GUI design allowed me to come up with something else that was so much easier to reason about and avoid those types of mistakes I tended to make, and one of the reasons I found it so much easier to reason about at least is because of these homogeneous loops and how they kinda resembled the control flow similar to when I was polling in the DOS days (even though it's not really polling and only performing work when there's work to be done). The idea was to move as far away from the event handling model as possible which implies non-homogeneous loops, non-homogeneous side effects, non-homogeneous control flows, and to work more and more towards homogeneous loops operating uniformly on homogeneous data and isolating and unifying side effects in ways that made it easier to just focus on "what" was happening, not "when" and "where", to reason about the correctness of the code.