Solutions for GTK interface lag, several methods for GTK child threads to control main thread UI updates

⌚Time: 2026-05-06 19:05:00

👨‍💻Author: Jack Ge

When GTK processes data, to avoid interface lag, it usually needs to use threads to handle operations, and then update the main thread UI in real time to refresh progress. However, GTK2/GTK3 itself is not thread-safe. Its main loop (g_main_loop_run) runs in the main thread and is responsible for handling all X11/Wayland events, redraw events, signal callbacks, etc. If another thread directly modifies the widget state, it will break the internal data structure state and cause a crash.

However, there are several ways to allow a child thread to safely update the GTK main thread UI while processing data.

Use g_idle_add

Example

#include <gtk/gtk.h>
#include <glib.h>
#include <stdio.h>
#include <unistd.h>

static GtkLabel *global_label;


static gboolean update_label(gpointer data) {
    int value = GPOINTER_TO_INT(data);
    
    char text[100];
    if (value < 0) {
        snprintf(text, sizeof(text), "Task completed!");
    } else {
        snprintf(text, sizeof(text), "Progress: %d%%", value);
    }
    gtk_label_set_text(global_label, text);
    
    return G_SOURCE_REMOVE;
}

// Child thread function
static gpointer thread_func(gpointer data) {
    for (int progress = 0; progress <= 100; progress += 20) {
        sleep(1);
        g_idle_add(update_label, GINT_TO_POINTER(progress));
    }
    
    // Send completion signal (use -1 to indicate completion)
    g_idle_add(update_label, GINT_TO_POINTER(-1));
    
    return NULL;
}

int main(int argc, char *argv[]) {
    if (!g_thread_get_initialized()) {
        g_thread_init(NULL);
    }
    gtk_init(&argc, &argv);
    
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(window), 300, 100);
    
    GtkWidget *label = gtk_label_new("waiting for data...");
    global_label = GTK_LABEL(label);
    
    gtk_container_add(GTK_CONTAINER(window), label);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    
    GThread *thread = g_thread_new("my_thread", thread_func, NULL);
    
    gtk_widget_show_all(window);
    gtk_main();
    
    g_thread_join(thread);
    return 0;
}

The reason g_idle_add can safely update the UI is that it does not update the UI directly in a child thread, but instead "dispatches" a function to the main thread's event queue, allowing the main thread to execute the UI update at the appropriate time.

Use gdk_threads_enter and gdk_threads_leave

gdk_threads_enter and gdk_threads_leave are global lock mechanisms provided in early versions of GTK2/GTK3, used to protect GDK (GTK's underlying drawing library) and ensure thread safety in a multithreaded environment. Simply put, they are a pair of lock/unlock functions that temporarily allow child threads to safely call GTK/GDK functions.

Call order, main thread

// Initialization
gdk_threads_init();
gtk_init(...);
//Other code
...
// The main thread starts executing
gdk_threads_enter();
gtk_main();
gdk_threads_leave();

Child thread

gdk_threads_enter();
//Update UI controls
...

gdk_threads_leave();

Example

#include <gtk/gtk.h>
#include <glib.h>
#include <unistd.h>

static GtkLabel *label;

static gpointer thread_func(gpointer data) {
    sleep(2);
    gdk_threads_enter();
    gtk_label_set_text(label, "refresh from child thread");
    gdk_threads_leave();
    sleep(2);
    return NULL;
}

int main(int argc, char *argv[]) {
    // Must be called before gtk_init
    gdk_threads_init();
    
    gtk_init(&argc, &argv);
    
    
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    label = GTK_LABEL(gtk_label_new("waiting for update"));
    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(label));
    
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    gtk_widget_show_all(window);
    
    GThread *thread = g_thread_new("worker", thread_func, NULL);
    
    // The main thread also needs to use a lock
    gdk_threads_enter();
    gtk_main();
    gdk_threads_leave();  //Release the lock after gtk_main ends
    
    g_thread_join(thread);
    return 0;
}

I have a question. Before the gtk_main() exits, it never executes gdk_threads_leave(), so it doesn't release the lock. Why can the child thread still use gdk_threads_enter()/gdk_threads_leave() to update the UI?

I asked AI, and its explanation was: gtk_main() automatically releases and reacquires the lock on each iteration, so most of the time, the lock is released (when GTK is waiting for events). The main thread only holds the lock during critical moments such as handling GTK callbacks or redrawing.

Use a timer to periodically obtain the progress information of a child thread

This method is relatively simple to understand. The child thread can lock and write the progress to a variable each time, and then the main thread can use a timer to periodically read the data and update the interface progress based on this data value. This can avoid child threads operating on the main thread's UI.

#include <gtk/gtk.h>
#include <glib.h>
#include <stdio.h>

typedef struct {
    GMutex mutex;
    int progress;
    gboolean working;
    char status[256];
} SharedData;

static SharedData shared = {0};

static gpointer worker_thread(gpointer data) {
    for (int i = 0; i <= 100; i += 10) {
        g_usleep(500000);  // 0.5 sec
        
        g_mutex_lock(&shared.mutex);
        shared.progress = i;
        snprintf(shared.status, sizeof(shared.status), "processing: %d%%", i);
        if (i >= 100) {
            shared.working = FALSE;
            snprintf(shared.status, sizeof(shared.status), "finished");
        }
        g_mutex_unlock(&shared.mutex);
        
    }
    return NULL;
}


static gboolean update_ui(gpointer data) {
    GtkWidget *progress_bar = GTK_WIDGET(data);

    // Lock and read shared data
    g_mutex_lock(&shared.mutex);
    int progress = shared.progress;
    gboolean working = shared.working;
    char status[256];
    strcpy(status, shared.status);
    g_mutex_unlock(&shared.mutex);
    
    // Safely update the UI (on the main thread)
    gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(progress_bar), progress / 100.0);

    printf("progress=%d, status=%s\n", progress, status);

    return working;  // TRUE continue, FALSE stop
}

int main(int argc, char *argv[]) {
    if (!g_thread_get_initialized()) {
        g_thread_init(NULL);
    }
    gtk_init(&argc, &argv);
    
    // Initialize shared data
    g_mutex_init(&shared.mutex);
    shared.progress = 0;
    shared.working = TRUE;
    strcpy(shared.status, "start...");
    
    // Create interface
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    GtkWidget *progress_bar = gtk_progress_bar_new();
    gtk_container_add(GTK_CONTAINER(window), progress_bar);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
    gtk_widget_show_all(window);
    
    // Create a timer (update the UI every 100 milliseconds)
    g_timeout_add(100, update_ui, progress_bar);
    
    // Create worker thread
    GThread *thread = g_thread_new("worker", worker_thread, NULL);
    
    // Enter the main loop
    gtk_main();
    
    // Clean
    g_thread_join(thread);
    g_mutex_clear(&shared.mutex);
    
    return 0;
}

Avoid using child threads

Some code is computation-intensive. If it is not particularly resource-consuming, it actually does not need to be executed independently in a thread and can be run directly in the main thread. However, it can cause a few seconds of operation stutter.

There is a way to alleviate this stutter. It creates a pseudo-smooth feeling. That is by using the gtk_events_pending() gtk_main_iteration() functions.

// When performing intensive computations on the main thread
for (int i = 0; i < 1000000; i++) {
    // Intensive computing...
    do_compute();
    
    // Handle events at intervals
    if (i % 1000 == 0) {
        while (gtk_events_pending()) {
            gtk_main_iteration();  // Handle a pending event
        }
    }
}

They can actively allow GTK to handle pending events during long computations. This alleviates issues such as unresponsive button clicks, unresponsive windows, and ineffective forced redraws. However, if the operation requires high real-time performance, there may still be some lag.