Executing GTK functions from other threads

前端 未结 1 697
天涯浪人
天涯浪人 2021-01-14 22:04

This question is about GTK and threads. You may find it useful if your application crashes, freezes or you want to have a multithreaded GTK application.

相关标签:
1条回答
  • 2021-01-14 22:48

    Main Loop

    In order to understand GTK you must understand 2 concepts.

    1. All contemporary GUIs are single-threaded. They have a thread which processes events from window system (like button, mouse events). Such a thread is called main event loop or main loop. GTK is also single threaded and not MT-safe. This means, that you must not call any GTK functions from other threads, as it will lead to undefined behaviour.

    2. As Gtk documentation sates,

    Like all GUI toolkits, GTK+ uses an event-driven programming model. When the user is doing nothing, GTK+ sits in the “main loop” and waits for input. If the user performs some action - say, a mouse click - then the main loop “wakes up” and delivers an event to GTK+. GTK+ forwards the event to one or more widgets.

    Gtk is event-based and asynchronous. It reacts to button clicks not in the exact moment of clicking, but a bit later.

    It can be very roughly written like this (don't try this at home):

    static list *pollable;
    
    int main_loop (void)
    {
        while (run)
            {
                lock_mutex()
                event_list = poll (pollable); // check whether there are some events to react to
                unlock_mutex()
                dispatch (event_list);        // react to events.
            }
    }
    
    void schedule (gpointer function)
    {
        lock_mutex()
        add_to_list (pollable, something);
        unlock_mutex()
    }
    

    I want a delayed action in my app

    For example, hide a tooltip in several seconds or change button text. Assuming your application is single-threaded, if you call sleep() it will be executed in main loop. sleep() means, that this particular thread will be suspended for specified amount of seconds. No work will be done. And if this thread is main thread, GTK will not be able to redraw or react to user interactions. The application freezes.

    What you should do is schedule function call. It can be done with g_timeout_add or g_idle_add In the first case our poll() from snippet above will return this event in several seconds. In the latter case it will be returned when there are no events of higher priority.

    static int count;
    
    gboolean change_label (gpointer data)
    {
        GtkButton *button = data;
        gchar *text = g_strdup_printf ("%i seconds left", --count);
        if (count == 0)
            return G_SOURCE_REMOVE;
        return G_SOURCE_CONTINUE; 
    }
    
    void button_clicked (GtkButton *button)
    {
        gtk_button_set_label (button, "clicked");
        count = 5;
        g_timeout_add (1 * G_TIME_SPAN_SECOND, change_label, button);
    }
    

    Returning a value from function is very important. If you don't do it, the behaviour is undefined, your task may be called again or removed.

    I have a long-running task

    Long-running tasks aren't different from calling sleep. While one thread is busy with that task, it can't perform any other tasks, obviously. If that is a GUI thread, it can't redraw interface. That's why you should move all long-running tasks to other threads. There is an exception, though: non-blocking IO, but it's out of topic of my answer.

    I have additional threads and my app crashes

    As already mentioned, GTK is not MT-safe. You must not call Gtk functions from other threads. You must schedule execution. g_timeout_add and g_idle_add are MT-safe, unlike other GTK functions. That callbacks will be executed in main loop. If you have some shared resources between callback and thread you must read/write them atomically or use a mutex.

    static int data;
    static GMutex mutex;
    
    gboolean change_label (gpointer data)
    {
        GtkButton *button = data;
        int value;
        gchar *text;
    
        // retrieve data
        g_mutex_lock (&mutex);
        value = data;
        g_mutex_unlock (&mutex);
    
        // update widget
        text = g_strdup_printf ("%i seconds left", --count);
        return G_SOURCE_REMOVE;
    }
    
    gpointer thread_func (gpointer data)
    {
        GtkButton *button = data;
        while (TRUE)
            {
                sleep (rand_time);
                g_mutex_lock (&mutex);
                ++data;
                g_mutex_unlock (&mutex);
                g_idle_add (change_label, button);
            }
    }
    

    Follow up: but python is single-threaded and GIL et cetera?

    You can imagine that python is multi-threaded application run on a single-core machine. You never know when the threads will be switched. You call a GTK function but you don't know in which state the main loop is. Maybe it free'd resources just a moment before. Always schedule.

    What is not discussed and further reading

    • Detailed documentation on glib main loop can be found here
    • GSource as a more low-level primitive.
    • GTask
    0 讨论(0)
提交回复
热议问题