Photon, the old GUI for QNX, supports some degree of multithreading. You can update the values of various display elements such as meters, progress bars, and text displays from other threads. That GUI is often used for control panels, with real-time data coming in that needs to be current on the display, which is why they made that work.
It's going to be interesting to see what happens when someone implements a new GUI in Rust. The classic problem with GUIs has been that ownership management for both allocation and locking was a big problem. Rust's borrow checker can help a lot with the bookkeeping needed to get that right.
I don't remember discussing either of those while writing Qt.
Rather, single-threading followed from two big points. First, the user calls the program rather than the other way around, and the user isn't multithreaded. Second, there aren't performance problems with the UI, and certainly none that require fighting #1.
Some programs need more than one thread. But that need does not originate within the UI, and complicating the UI for it would comply with RFC 1925 point 5.
Locks turn out to be unnecessary for multithreaded code. A ringbuffer of messages is only slightly more complex, yet the gains are massive. Both in performance and simplicity.
It's possible to manage a ringbuffer without any locks. The trick is to have a counter for the producer thread, and a counter for each consumer thread. Whenever the producer wants to know "Is it safe to add a message?" it takes the minimum of all consumer counters, modulo the size of the ringbuffer. The result is the smallest index that the producer must not write beyond.
In other words, you always know when you're producing messages too quickly and need to wait on the consumers. And the consumers know when there's a message waiting -- they just look at the producer's counter. Blazingly fast, and no locks. Cool trick!
Ringbuffers (or any queue, actually) don't solve the problems associated with locks. Often when people talk about locks when discussing concurrent algorithms, they don't necessarily mean the particular construct called a lock but any synchronization mechanism that might end up suspending some computation when waiting for another to happen, so a queue may well be a lock in this kind of discussion.
To see the duality between locks and queues note that any queue can be implemented with any list/array and a lock, and a lock itself is nothing more than some atomic operation, plus a queue plus a mechanism to suspend computation. Whether that suspension involves an actual parking of the kernel thread or spinning, is an implementation detail from the perspective of the algorithm.
You can use queues without deadlocks, but then you won't have the same advantages locks can give you (transactions), or you can have the same advantages, but then get the same problems.
There are multiple ways to make a lock-free ringbuffer; most use some sort of similar trick with per-consumer atomic counters, though I hadn't heard of something as simple as the minimum-modulo trick described in the gp! Some implementations:
I highly recommend Nitsan Wakart's blog. He covers a lot of interesting ground, all in Java. Probably best to start at the early blog posts and work your way forward.
One can probably argue it's not even more complex at all.
Locks just look simple because it's "just chuck a mutex around it", in reality though it's all a mirage. Locks are comfortable, not simple - is how I like to put it at least.
It's going to be interesting to see what happens when someone implements a new GUI in Rust. The classic problem with GUIs has been that ownership management for both allocation and locking was a big problem. Rust's borrow checker can help a lot with the bookkeeping needed to get that right.