首页 > 解决方案 > 从其他线程执行 GTK 函数

问题描述

这个问题是关于 GTK 和线程的。如果您的应用程序崩溃、冻结或您想要拥有一个多线程 GTK 应用程序,您可能会发现它很有用。

标签: multithreadinggtk3

解决方案


主循环

要了解 GTK,您必须了解 2 个概念。

  1. 所有当代 GUI 都是单线程的。他们有一个线程来处理来自窗口系统的事件(如按钮、鼠标事件)。这样的线程称为主事件循环或主循环。GTK 也是单线程的,不是MT 安全的。这意味着,您不能从其他线程调用任何 GTK 函数,因为它会导致未定义的行为。

  2. 正如 Gtk 文档所述,

与所有 GUI 工具包一样,GTK+ 使用事件驱动的编程模型。当用户什么都不做时,GTK+ 位于“主循环”并等待输入。如果用户执行了一些动作——比如说,鼠标点击——然后主循环“唤醒”并将一个事件传递给 GTK+。GTK+ 将事件转发到一个或多个小部件。

Gtk 是基于事件和异步的。它对按钮点击的反应不是在点击的确切时刻,而是稍后。

可以很粗略地写成这样(不要在家里尝试这个):

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()
}

我想在我的应用程序中执行延迟操作

例如,在几秒钟内隐藏工具提示或更改按钮文本。假设您的应用程序是单线程的,如果您调用sleep()它将在主循环中执行。 sleep()意味着,这个特定的线程将被挂起指定的秒数。不会做任何工作。如果这个线程是主线程,GTK 将无法重绘或对用户交互做出反应。应用程序冻结。

你应该做的是安排函数调用。可以使用g_timeout_addg_idle_add 在第一种情况下,我们上面的poll()from 代码段将在几秒钟内返回此事件。在后一种情况下,它将在没有更高优先级的事件时返回。

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);
}

从函数返回值非常重要。如果您不这样做,则行为未定义,您的任务可能会被再次调用或删除。

我有一个长期运行的任务

长时间运行的任务与调用sleep. 当一个线程忙于那个任务时,它显然不能执行任何其他任务。如果那是一个GUI线程,它不能重绘界面。这就是为什么您应该将所有长时间运行的任务移动到其他线程。不过有一个例外:非阻塞 IO,但这超出了我的回答范围。

我有额外的线程并且我的应用程序崩溃了

如前所述,GTK 不是 MT 安全的。你不能从其他线程调用 Gtk 函数。您必须安排执行。g_timeout_add并且g_idle_add MT 安全的,与其他 GTK 函数不同。该回调将在主循环中执行。如果您在回调和线程之间有一些共享资源,则必须以原子方式读取/写入它们或使用互斥锁。

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 ("Current data value: %i", value);
    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);
        }
}

确保尽可能少地持有互斥锁。想象一下,您将一个互斥锁锁定在另一个线程中并执行一些 IO。主循环将被卡住,直到互斥锁被释放。有g_mutex_try_lock()一个立即返回,但它会带来额外的同步问题,因为你不能保证当 mainloop 尝试锁定互斥锁时它会被解锁。

跟进:但是python是单线程的,GIL等等?

你可以想象 python 是在单核机器上运行的多线程应用程序。你永远不知道线程什么时候会被切换。您调用了 GTK 函数,但您不知道主循环处于哪个状态。也许它刚刚释放了资源。总是安排。

未讨论的内容和进一步阅读

  • 关于 glib 主循环的详细文档可以在这里找到
  • GSource作为更底层的原语。
  • GTask

推荐阅读