C语言 GTK3和多线程,替换弃用的函数

b09cbbtk  于 2023-06-21  发布在  其他
关注(0)|答案(4)|浏览(137)

我想在使用线程的应用程序中替换已弃用的函数gdk_threads_enter()/leave()。现在的应用程序工作得很完美(* 尽管我不确定这是否是正确的方法 *)。
我的主循环运行gtk_main和信号处理程序。当我收到一个开始按钮时,我启动一个线程,该线程在后台沿着主线程运行。我怎么能从那个线程更新GUI。我知道根据GTK 3和GDK 3的文档,他们说通过使用

gdk_threads_add_idle()

gdk_threads_add_timeout()

但是,如果我希望仅在单击“开始”时进行更新,该如何执行此操作?有没有例子。我不是在问如何使用gdk_threads_add_idle(),我是在问如何在没有线程的情况下在主线程中运行worker函数。

点击按钮-->启动worker函数“in thread previously”-->更新GUI窗口中的大量GUI元素。

tzdcorbm

tzdcorbm1#

你有三种方法来做到这一点:
1.在按钮回调中进行计算,使用gtk_event_pending()/gtk_main_iteration()
1.使用g_idle_add()或其他,以及gtk_event_pending()/gtk_main_iteration()
1.使用一个线程,最后是一个互斥体,以及g_idle_add()或其他。通常情况下,互斥是不需要的,但它可以解决一些bug或Heisenbugs
第三种解决方案似乎是最好的,因为使用前两种方法时,我在计算运行时退出应用程序时遇到了一些问题。应用程序没有退出,并且打印了很多“Gtk Critical”警告。(* 我在Windows和mingw 32 * 上试过)。

1.按钮回调:

如果要在主gtk循环中运行辅助线程,可以直接在按钮回调中进行计算,更新GUI并使用gtk_event_pending()gtk_main_iteration()处理事件,如以下示例代码所示:

void on_button_clicked(GtkButton * button, gpointer data) {

  // do some computation...

  // modify the GUI:
  gtk_label_set_text(label,"text");

  // run the main iteration to update the GUI,
  // you need to call these functions even if the GUI wasn't modified,
  // in order to get it responsive and treat events from it:
  while(gtk_events_pending()) gtk_main_iteration();

  // do some other computation...

  // huge computation in a loop:
  while(1) {
    // do some computation...

    // update the GUI and treat events from it:
    while(gtk_events_pending()) gtk_main_iteration();
  }
}

2. g_idle_add():

您也可以使用gdk_thread_add_idle()(在某些不受您控制的库可能使用gdk_threads_enter()/leave()的情况下)或g_idle_add()g_main_context_invoke()来代替g_thread_new()

gboolean compute_func(gpointer data) {

  // do some computation...

  // modify the GUI:
  gtk_label_set_text(label,"text");
  // run the main loop to update the GUI and get it responsive:
  while(gtk_events_pending()) gtk_main_iteration();

  // do some other computation...

  // huge computation in a loop:
  while(1) {
    // do some computation...

    // update GUI and treat events from it:
    while(gtk_events_pending()) gtk_main_iteration();
  }

  return FALSE;
}

void on_button_clicked(GtkButton * button, gpointer data) {

    g_idle_add(compute_func,data);
}

3.线程和互斥:

在 * 某些 * 情况下,使用线程可以使计算更快,因此当使用不在主gtk循环中的工作线程时,以及当使用工作线程中的gdk_threads_add_idle()g_idle_add()更新添加到主循环中的函数中的GUI时,您可能必须使用互斥锁锁定对GUI的访问,因为访问GUI的函数之间可能存在冲突。互斥体必须在被应用程序使用之前用g_mutex_init(&mutex_interface);初始化。例如:

GMutex mutex_interface;

gboolean update_gui(gpointer data) {
  g_mutex_lock(&mutex_interface);
  // update the GUI here:
  gtk_button_set_label(button,"label");
  // And read the GUI also here, before the mutex to be unlocked:
  gchar * text = gtk_entry_get_text(GTK_ENTRY(entry));
  g_mutex_unlock(&mutex_interface);

  return FALSE;
}

gpointer threadcompute(gpointer data) {
  int count = 0;

  while(count <= 10000) {
    printf("\ntest %d",count);
    // sometimes update the GUI:
    gdk_threads_add_idle(update_gui,data);
    // or:
    g_idle_add(update_gui,data);

    count++;
  }

  return NULL;
}

void on_button_clicked(GtkButton * button, gpointer data) {

    g_thread_new("thread",threadcompute,data);
}

如果需要更新GUI的函数以特定顺序执行,则需要添加两个计数器,并为每个使用g_idle_add()gdk_threads_add_ilde()调用的函数分配一个编号:

GMutex mutex_interface;

typedef struct _data DATA;
struct _data {
  gchar label[1000];
  GtkWidget * w;
  int num;
};

int counter = 0;
int counter2 = 0;

gboolean update_gui(gpointer data) {
  DATA * d = (DATA *)data;

  debutloop:
  g_mutex_lock(&mutex_interface);
  if(d->num != counter2) {
    g_mutex_unlock(&mutex_interface);
    goto debutloop;
  }
  counter2++;
  // update the GUI here:
  gtk_button_set_label(GTK_BUTTON(d->w),d->label);
  // And read the GUI also here, before the mutex to be unlocked:
  gchar * text = gtk_entry_get_text(GTK_ENTRY(entry));
  g_mutex_unlock(&mutex_interface);

  free(d);

  return FALSE;
}

gpointer threadcompute(gpointer data) {
  int count = 0;

  while(count <= 10000) {
    printf("\ntest %d",count);

    DATA * d = (DATA*)malloc(sizeof(DATA));
    sprintf(d->label,"%d",count);
    d->w = (GtkWidget*)data;
    d->num = counter;
    counter++;
    // update the GUI:
    g_idle_add(update_gui,d);

    count++;
  }
  return NULL;
}

void on_button_clicked(GtkButton * button, gpointer data) {

    g_thread_new("thread",threadcompute,button);
}

我还测试了锁定单个小部件而不是整个GUI的情况,它似乎可以工作。

bvn4nwqk

bvn4nwqk2#

文档中说的是,你仍然可以在线程中运行你的worker函数,只是不能在该线程中使用GTK和GDK函数。因此,您仍然可以在单击“开始”时启动线程。但是,您必须使用gdk_threads_add_idle()将GUI元素调度为从主线程更新,而不是从线程更新。
所以你的图表应该看起来像这样:

Main thread     Worker thread
    |
Button clicked
    |      \________
    |               \
    |           Start worker function
    |                |
    |           Computation
    |                |
    |           Want to update GUI
    |                |
    |           gdk_threads_add_idle(function1, data1)
    | ______________/|
    |/               |
    v           More computation
function1 runs       |
    |           Want to update GUI
GUI updated          |
    |           gdk_threads_add_idle(function2, data2)
    | ______________/|
    |/               |
    v           More computation
function2 runs       |
    |      
  etc...

如果这对于你的用例来说太复杂了,并且你的工作线程中有一个计算经常将控制权返回给你的工作线程(比如,你正在循环中计算一些东西),那么你可以通过短暂地将控制权返回给GUI主循环来完全在主线程中运行计算而不锁定GUI,如下所示:

for (lots of items) {
    result = do_short_calculation_on(one_item);

    update_gui(result);

    while (gtk_events_pending())
        gtk_main_iteration();
}
af7jpaap

af7jpaap3#

当我关闭主窗口时,我得到这个运行错误:Gtk-Critical**:gtk_widget_get_parent:Assert“GTK_IS_WIDGET(widget)”失败
我想我已经找到了一个解决方案,使用两个全局变量来指示回调停止并调用gtk_main_quit(),并将主窗口的“destroy”信号捕获到一个名为gtk_main_quit2()的自定义回调中,如以下示例所示:

int process_running = 0; // indicate if the "process" is running
int stopprocess = 0; // indicate to the callback to stop or not

void gtk_main_quit2(GtkWidget * window, gpointer data) {
  if(process_running == 0) gtk_main_quit(); // if the "process" isn't running
                                            // then quit

  stopprocess = 1; // indicate to the button callback to stop and quit
}

void on_button_clicked(GtkButton * button, gpointer data) {

  // indicate the "process" is running:
  process_running = 1;

  // do some computation...

  while(gtk_events_pending()) gtk_main_iteration();
  if(stopprocess == 1) {
    // if close button clicked then quit:
    gtk_main_quit();
    return;
  }

  // do some other computation...

  // huge computation in a loop:
  while(1) {
    // do some computation...

    while(gtk_events_pending()) gtk_main_iteration();
    if(stopprocess == 1) {
      // if close button clicked then quit:
      gtk_main_quit();
      return;
    }
  }

  while(gtk_events_pending()) gtk_main_iteration();
  // indicate the "process" is finished:
  process_running = 0;
  // in the case the user clicked close button just at the end of computation:
  if(stopprocess == 1) {
    gtk_main_quit();
    return;
  }
}

int main() {

  gtk_init();
  Gtkwidget * window = create_window();
  g_signal_connect ((gpointer) window, "destroy", G_CALLBACK(gtk_main_quit2), NULL);
  gtk_main();

}

如果你在点击关闭按钮后仍然有一些gtk警告,你可以尝试捕获“delete-event”信号而不是主窗口上的“destroy”信号。

e0bqpujr

e0bqpujr4#

这是一个老问题,但我想我会继续并添加一个更好的方法来做到这一点:
首先,Bertrand的方法1和2的问题是,在UI线程上进行长时间运行的线程是不可取的,即使调用gtk_main_iteration()来服务未决事件,如可以看到的,然后将其捆绑到涉及关闭和删除事件的角落情况中,并且进一步地,如果从太多的小部件进行此操作,则可能导致堆栈溢出,这些小部件都在进行长时间运行的工作。调用gtk_events_pending()gtk_main_iteration()看起来就像是一个脆弱的创可贴解决方案,它可能适用于较短的操作,其中需要在快速执行某些操作的同时保持UI活动,但对于长时间运行的网络操作,这似乎不是一个好的设计模式,最好将其放入与UI完全分离的自己的线程中。
现在,如果希望从这样一个长时间运行的线程更新UI,例如进行几次网络传输并报告状态,则可以使用管道进行线程间通信。使用像Bertrand的方法3中的互斥锁的问题是获取锁可能会很慢,并且如果长时间运行的线程已经获取了锁,则可能会阻塞,特别是Bertrand循环回debutLoop的方式,这会导致UI线程停止等待计算线程,这是不可接受的。
然而,使用管道,可以以非阻塞方式与UI线程通信。
本质上,在程序开始时从FIFO文件创建一个非阻塞管道,然后可以使用gdk_threads_add_idle创建一个用于从后台线程接收消息的哨兵线程,如果有定时器线程经常检查URL以使用HTTP事务的结果更新UI元素,则该哨兵函数甚至可以存在于程序的整个生命周期。
例如:

/* MyRealTimeIP, an example of GTK UI update thread interthread communication
 *
 * Copyright (C) 2023 Michael Motes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#include <gtk/gtk.h>
#include <fcntl.h>
#include <pthread.h>
#include <curl/curl.h>
#include <sys/stat.h>
#include <stdlib.h>

#define APP_NAME            "MyRealTimeIP"
#define MY_IP_LABEL         "My IP: "
#define UPDATE_TIME_LABEL   "Updated at: "

#define FIFO_FILE "/tmp/" APP_NAME "_Pipe"
#define IP_CHECK_URL "https://domains.google.com/checkip"
#define NETWORK_ERROR_TIMEOUT 1 //one second cooldown in case of curl error
#define PIPE_TIMEOUT 50000000 //50ms timeout between pipe checks to lower CPU usage

#define IP_CHECK_URL "https://domains.google.com/checkip"
#define UNEXPECTED_ERROR (-1)
#define MEMORY_ERROR "\nMemory allocation failed.\n"
#define MEMCHECK(x)                        \
        if((x)== NULL){                    \
            fprintf(stderr, MEMORY_ERROR); \
            exit(UNEXPECTED_ERROR);        \
        }

#define TIME_FMT_LEN 45
#define CURRENT_TIME_STR(timeStr){                  \
    struct timespec rt_clock = {};                  \
    clock_gettime(CLOCK_REALTIME,&rt_clock);        \
    time_t raw_time;                                \
    struct tm *time_info;                           \
    time(&raw_time);                                \
    time_info = localtime(&raw_time);               \
    /*If this is ever not true it means the
      hour changed between clock_gettime call
      and localtime call, so I update the values
      unless it would roll back the day, in that case
      I just roll forward nanoseconds to 0.*/       \
    if(time_info->tm_hour - (daylight ? 1 :  0)     \
            + timezone/3600 !=                      \
                      (int)((rt_clock.tv_sec / 3600)\
                                              % 24))\
    {                                               \
        if(time_info->tm_hour == 0)  {              \
            rt_clock.tv_nsec = 0;                   \
        }else{                                      \
            time_info->tm_hour =                    \
                      (int)((rt_clock.tv_sec / 3600)\
                                              % 24);\
            time_info->tm_sec =                     \
                        (int)(rt_clock.tv_sec % 60);\
            time_info->tm_min =                     \
                        (int)((rt_clock.tv_sec / 60)\
                                              % 60);\
             }                                      \
    } else {                                        \
        time_info->tm_sec =                         \
                    (int)(rt_clock.tv_sec % 60);    \
        time_info->tm_min =                         \
                    (int)((rt_clock.tv_sec / 60)    \
                                          % 60);    \
    }                                               \
                                                    \
    timeStr = malloc(TIME_FMT_LEN);                 \
    snprintf(timeStr,TIME_FMT_LEN,                  \
            "%04d-%02d-%02d %02d:%02d:%02d.%03d",   \
            time_info->tm_year + 1900,              \
            time_info->tm_mon + 1,                  \
            time_info->tm_mday,                     \
            time_info->tm_hour,                     \
            time_info->tm_min,                      \
            time_info->tm_sec,                      \
            (int)(rt_clock.tv_nsec/1000000));       \
}

#pragma region IO_Macros
#define READ_BUF_SET_BYTES(fd, buffer, numb, bytesRead){  \
    ssize_t rb = bytesRead;                               \
    ssize_t nb;                                           \
    while (rb < numb) {                                   \
        nb = read(fd,(char*)&buffer + rb,numb - rb);      \
        if(nb<=0)                                         \
            break;                                        \
        rb += nb;                                         \
    }                                                     \
    bytesRead = rb;                                       \
}
#define READ_BUF(fd, buffer, numb) {                   \
        ssize_t bytesRead = 0;                         \
        READ_BUF_SET_BYTES(fd, buffer, numb, bytesRead)\
}

#define WRITE_BUF(fd, buf, sz){                \
    size_t nb = 0;                             \
    size_t wb = 0;                             \
    while (nb < sz){                           \
        wb = write(fd, &buf + nb, sz-nb);      \
        if(wb == EOF) break;                   \
        nb += wb;                              \
    }                                          \
}
#pragma  endregion

GtkWidget *my_IP_Label;
GtkWidget *updatedTimeLabel;
static int interthread_pipe;

enum pipeCmd {
    SET_IP_LABEL,
    SET_UPDATED_TIME_LABEL,
    IDLE
};

typedef struct {
    size_t size;
    char *str;
} curl_ret_data;

static void fifo_write(enum pipeCmd newUIcmd);

static void fifo_write_ip(char *newIP_Str);

static void fifo_write_update_time(char *newUpdateTimeStr);

static gboolean ui_update_thread(gpointer unused);

static void *ui_update_restart_thread(void *);

static size_t curl_write_data(void *in, size_t size, size_t nmemb, curl_ret_data *data_out);

static void *checkIP_thread(void *);

int main(int argc, char *argv[]) {
    mkfifo(FIFO_FILE, 0777);
    interthread_pipe = open(FIFO_FILE, O_RDWR | O_NONBLOCK);

    gtk_init(&argc, &argv);

    GtkWidget *appWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW (appWindow), APP_NAME);
    gtk_widget_set_size_request(appWindow, 333, 206);

    GtkBox *vbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));

    my_IP_Label = gtk_label_new(MY_IP_LABEL "Not updated yet.");
    updatedTimeLabel = gtk_label_new(UPDATE_TIME_LABEL "Not updated yet.");

    gtk_box_pack_start(vbox, my_IP_Label, TRUE, FALSE, 0);
    gtk_box_pack_start(vbox, updatedTimeLabel, TRUE, FALSE, 0);

    gtk_container_add(GTK_CONTAINER (appWindow), GTK_WIDGET(vbox));

    gtk_widget_show_all(appWindow);
    g_signal_connect (G_OBJECT(appWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pthread_t checkIP_thread_pid;
    if (pthread_create(&checkIP_thread_pid, NULL, &checkIP_thread, NULL) != 0)
        return UNEXPECTED_ERROR;

    gdk_threads_add_idle(ui_update_thread, NULL);

    gtk_main();

    pthread_cancel(checkIP_thread_pid);
    pthread_join(checkIP_thread_pid, NULL);

    return 0;
}

size_t curl_write_data(void *in, size_t size, size_t nmemb, curl_ret_data *data_out) {
    size_t index = data_out->size;
    size_t n = (size * nmemb);
    char *temp;

    data_out->size += (size * nmemb);

    temp = realloc(data_out->str, data_out->size + 1);
    MEMCHECK(temp)
    data_out->str = temp;
    memcpy((data_out->str + index), in, n);
    data_out->str[data_out->size] = '\0';

    return size * nmemb;
}

_Noreturn void *checkIP_thread(void *unused) {
    sleep(2); //not needed, just for example purposes to show initial screen first
    while (1) {
        CURL *curl;
        CURLcode res;
        curl_ret_data data = {};
        while (data.str == NULL) {
            curl = curl_easy_init();
            if (curl) {
                curl_easy_setopt(curl, CURLOPT_URL, IP_CHECK_URL);
                curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
                curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_data);
                curl_easy_setopt(curl, CURLOPT_WRITEDATA, &data);
                res = curl_easy_perform(curl);
                if (res != CURLE_OK) {
                    fprintf(stderr, "curl_easy_perform() failed: %s\n",
                            curl_easy_strerror(res));
                    if (data.str != NULL) {
                        free(data.str);
                        data.str = NULL;
                    }
                    sleep(NETWORK_ERROR_TIMEOUT);
                }
                curl_easy_cleanup(curl);
            }
        }
        int newIP_StrSz = strlen(MY_IP_LABEL) + data.size + 1;
        char *newIP_Str = calloc(1, newIP_StrSz);
        snprintf(newIP_Str, newIP_StrSz, MY_IP_LABEL " %s", data.str);
        fifo_write_ip(newIP_Str);

        char *timeStr;
        CURRENT_TIME_STR(timeStr)
        int newUpdateTimeStrSz = strlen(UPDATE_TIME_LABEL) + TIME_FMT_LEN + 1;
        char *newUpdateTimeStr = calloc(1, newUpdateTimeStrSz);
        snprintf(newUpdateTimeStr, newUpdateTimeStrSz, UPDATE_TIME_LABEL " %s", timeStr);
        free(timeStr);
        fifo_write_update_time(newUpdateTimeStr);

        sleep(5);
    }
}

static void fifo_write(enum pipeCmd newUIcmd) {
    WRITE_BUF(interthread_pipe, newUIcmd, sizeof(newUIcmd))
}

static void fifo_write_ip(char *newIP_Str) {
    fifo_write(SET_IP_LABEL);
    WRITE_BUF(interthread_pipe, newIP_Str, sizeof(newIP_Str))
}

static void fifo_write_update_time(char *newUpdateTimeStr) {
    fifo_write(SET_UPDATED_TIME_LABEL);
    WRITE_BUF(interthread_pipe, newUpdateTimeStr, sizeof(newUpdateTimeStr))
}

gboolean ui_update_thread(gpointer unused) {
    enum pipeCmd pipeBuffer = IDLE;

    READ_BUF(interthread_pipe, pipeBuffer, sizeof(pipeBuffer))
    switch (pipeBuffer) {
        case SET_IP_LABEL: {
            char *newIP_Str = NULL;
            int bytesRead = 0;
            while (bytesRead != sizeof(newIP_Str)) {
                READ_BUF_SET_BYTES(interthread_pipe, newIP_Str, sizeof(newIP_Str) - bytesRead, bytesRead)
            }
            gtk_label_set_text(GTK_LABEL(my_IP_Label), newIP_Str);
            free(newIP_Str);
            break;
        }
        case SET_UPDATED_TIME_LABEL: {
            char *newUpdateTimeStr = NULL;
            int bytesRead = 0;
            while (bytesRead != sizeof(newUpdateTimeStr)) {
                READ_BUF_SET_BYTES(interthread_pipe, newUpdateTimeStr, sizeof(newUpdateTimeStr) - bytesRead, bytesRead)
            }
            gtk_label_set_text(GTK_LABEL(updatedTimeLabel), newUpdateTimeStr);
            free(newUpdateTimeStr);
            break;
        }
        case IDLE:
            break;
    }
    

    //Return false to detach update ui thread, reattach it after a timeout so CPU doesn't spin unnecessarily.
    pthread_t _unused;
    if (pthread_create(&_unused, NULL, ui_update_restart_thread, NULL))
        exit(UNEXPECTED_ERROR);
    return FALSE;
}

static void *ui_update_restart_thread(void *unused) {
    struct timespec delay = {0, PIPE_TIMEOUT};
    nanosleep(&delay, NULL);
    gdk_threads_add_idle(ui_update_thread, NULL);
    return NULL;
}

相关问题