Event programming problem because of Rc vs Arc

Hello,
Here is a pseudo code to show the problem:

struct Label {
    pub text: String,
}
impl Label {
    fn get_text(&self) -> String {
        self.text.clone()
    }
    fn set_text(&mut self, text: String) {
        self.text = text;
    }
}
struct Button;
impl Button {
    fn on_click(&self, callback: impl Fn()) {
        std::thread::spawn(|| {
            callback(); // error: must add Send+Sync
        });
    }
}

I know your answer: Add Send+Sync then :-1:

The problem is GUI is running inside a single thread: ui thread, so the closure must be of type dyn Fn(), but not dyn Fn()+Send+Sync. This way you can move the label inside the closure without problem using Rc instead of Arc.
On user interaction, click event is running from outside the ui thread like in my pseudo code. And I think the way is not to promote every ui components to an Arc: such a case will tell that rust doesn't need Box, Rc... but only Arc.
I tried a mspc channel, but rx blocks the ui thread: I need something like: rx.on_recv(||{callback();}); to run the callback on ui thread every time a click is received.
I don't want to rely on a global event loop because of the overhead and I want each component to manage their events internally.

I hope your understand my requirement.
Thank you in advance for your help.

it depends on what UI system or framework you are using. most UI libraries have some mechanism to "wake up" the event loop by sending user defined messages.

for example, in winit, you can use EventLoopProxy::send_event(). some UI event loop can also serve as an async runtime, such as glib::MainContext used by gtk.

if you are using gtk, MainContext::spawn() or MainContext::invoke() may be interesting to you.

3 Likes

Well, it doesn't rely on any UI framework.
It is just a pseudo code to illustrate my problem which is more global and related to events and threading without the unnecessary dyn Fn()+Send+Sync

Kinda not. You have hit the problem with most modern UI frameworks: that code that your wrote would never work whether you would use Arc or Rc…

That's the problem, isn't it? All Most UI frameworks have that problem, yet solutions are different, depending on which one do you use.

Without knowing which frameworks do you use and which approach does it use to solve the issue of interaction with “background threads” it's hard to propose a way to solve your dilemma.

But Rust hit the nail exactly on it's head, without even trying: most UI frameworks assume that you wouldn't do what you are attempting to do and would, instead, use some kind of “loose coupling” schema.

P.S. I remember around 20 years Google in Russia asked candidates to write a simple program that would sort 10 GiGabytes of strings in a text file and key requiremenets was to have “Stop” button. You wouldn't believe how many seemingly experienced developers presented solutions with button that was working perfectly… except it waited till file would be fully sorted before any reaction may be observed.

1 Like

Are you building a custom UI framework? Or just using UI as an example?

If you want the ability to run callbacks on a thread from another thread you'll need an event loop or something like that on the ui thread.

1 Like

Something should be responsible for managing the control flow. If some function (lets call it f) is currently running on a ui thread, with spawn the callback will run on a separate core, in parallel. With your on_event, your ui thread would have to gain the control flow back from f and those who called it, understand that an event happened, and schedule that work. So this is 100% dependent on the UI framework, because it is the job of the framework to schedule everything, this is the problem it is meant to solve.

Additionaly, your problem is very similar to what async is meant to handle. Something like LocalSet in tokio::task - Rust seems like a good fit: the spawned tasks will be pinned to the concrete thread and tokio will make sure control flow reaches them.

3 Likes

I'm a little lost:

  • Rc doesn't need to be upgraded to an Arc (my requirement or rust requirement to maximize speed over usability) :check_mark:
  • rx.on_recv(||{callback();}); doesn't exist (yet: would really solve my problem I think) :check_mark:

So, I must implement the event system by myself, but I can't find a clever way to implement without using a timer or an infinite loop.

How does the code uphold the requirement written here? The code explicitly references the callback in a new thread. The callback must be Sync for this operation to be legal.

You either want multithreading as shown in the code, or you want single threaded as written in the requirement. It can't be both.

  1. If you require multithreading, you can weaken the constraint to impl Fn() + Send + 'static with a move closure.
  2. If you require single threaded, you need that thread to run an event loop. The simplest possible implementation is an MPSC consumer with enum messages:
while let Ok(msg) = self.receiver.recv() {
    match msg {
        Message::OnClick(id) => {
            // Call registered callback for widget id
            let callback = &self.callbacks[id];
            callback();
        }
        _ => {
            // Handle other messages...
            todo!()
        }
    }
}

Callbacks need to be registered with the event loop separately.

I am personally not a fan of this design. If I need to write an event loop anyway (and I always do!) then I may as well hardcode the event handling. It's not like any of my UIs have ever required dynamic emergent behavior! The "open file" button always opens a file. It is absurd to build a system that can change that button's behavior at runtime.

This is much better:

match msg {
    Message::OpenFile => {
        // Yep, we only need to open a file.
    }
    Message::Save => {
        // And so on ...
    }
}

Which looks a lot like The Elm Architecture... Which itself is a reflection of the way GUI event handling has always been written.

Correct. You need an infinite loop. The clever part is idling while there is nothing to do and wake up immediately when some new task is made available.

5 Likes

I think you're not too consistent, maybe that's why it is complicated. You mention a UI thread but plan to move label inside closure possibly for modification. Isn't that a UI operation that should run in current thread? You should clear the boundary of each thread tasks.

To be more consistent, all related UI operations must run in a closure in the same thread (yeah, no need to Sync+Send). Then, you have other thread for long running operation.

As for the interaction handler, if you don't want to implement an event loop, you may use other library. That library should listen an input for you and call the appropriate onclick handler in async way, as your requirement pointed out.

Next, you may use fixed number of thread (one for UI, one for other thing) and use MPSC to communicate or initiate certain operation. Then, model possible operation as function that manage the communication:

fn download(ops: OpsEnum) {
    //
    tx.send(ops)
}
// Only OpsEnum need to be Send

Possibly using async function, I don't know. In fact we just need to call it (inside the closure callback) without being bothered about spawning thread or callback Sync+Send.

Quite long, but only words without code..

Don't know which could be the accepted answer, but this discussion helps me a lot...

LocalSet would be the solution if using Tokio IMO.