How to separate the update of the egui GUI and background program processes?

Let's say you are building a GUI application with Egui.
You want to make start a certain task when you click a button.
You can simply write code like this.
if ui.button("click me").clicked() {
start_task();
}
But what if that task takes 10 seconds to be completed?
Egui is an immediate mode GUI library, which means, you show the button and interact with it immediately, doing so every frame (e.g. 60 times per second). So the UI freeze until the task is done. In your case, that will be more than a couple of seconds. This is not good.
You need to seperate the update of the UI and running task.
You can achieve this by using a thread.
Update Function in Egui and Thread
This is the basic code for rendering a button to execute task when clicked:
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if ui.button("click me").clicked() {
start_task();
}
}
}
}
The update
function is called to render UI, and as mentioned earlier, it is called every frame. When the button is clicked, the start_task
function will block the update
function and it will block rendering until the task is done.
We can create a new thread and call stark_task
inside it to avoid blocking redering. Rust provide threadㄴ with standard library. A new thread can be spanwed using the thread::spawn function.
use std::thread;
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if ui.button("click me").clicked() {
thread::spawn(move || {
start_task();
}
}
}
}
}
Send Siganl from Thread to communicate
Suppose you want to show some kind of notification message when the task is done. To do this, the rendering thread and task thread need to communicate. Rust provides channel for this. The channel function returns a sender and a receiver.
The idea is to create a sender and receiver. When the task is done, send a signal using the sender and receive the signal using the receiver. The important part is to store the sender and receiver as fields of a struct to keep the references to them. (Remember, update is called each frame, so if you create them inside it, it will create a new channel every frame.)
Let's take a look at an example:
use std::thread;
use std::sync::mpsc::{channel, Receiver, Sender, TryRecvError};
// define fields for sender and receiver
struct MyApp {
tx: Sender<()>,
rx: Receiver<()>,
}
impl MyApp {
pub fn new() {
// initialize sender, receiver
let (tx, rx) = channel();
Self {
tx,
rx,
}
}
}
Here, we define fields and a constructor for the sender and receiver. Now let's use them inside the update function.
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
if ui.button("click me").clicked() {
let tx = self.tx.clone(); // clone the sender to use it inside thread
thread::spawn(move || {
start_task();
tx.send(()).expect("failed to send result"); // send signal after function call
}
}
// receive the signal
let result = self.rx.try_recv();
match result {
Ok(something) => {
// the task is done. do something
}
Err(e) => {
if e == TryRecvError::Disconnected {
error!("{:?}", e);
}
}
}
}
}
}
You can send a signal with data such as an integer or boolean, and the receiver will have it once the signal is received.
Now you can add any code inside the Ok(_) block to do whatever you want.
With this approach, you can ensure your UI remains responsive while handling long-running tasks in the background.
Subscribe to my newsletter
Read articles from Baku Kim directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Baku Kim
Baku Kim
With 2+ years of experience in web backend development, I now specialize in AI engineering, building intelligent systems and scalable solutions. Passionate about crafting innovative software, I love exploring new technologies, experimenting with AI models, and bringing ideas to life. Always learning, always building.