GTK3 и многопоточность, замена устаревших функций
Я хотел бы заменить устаревшие функции gdk_threads_enter()/leave()
в моем приложении, которое использует потоки. Приложение, как оно есть сейчас, работает отлично (хотя я не уверен, что это правильный способ сделать это).
Мой основной цикл, запускает gtk_main
и обработчики сигналов. Когда я получаю кнопку запуска, я запускаю поток, который проходит в фоновом режиме вдоль основного. Как я могу обновить графический интерфейс из этой темы. Я знаю по документации GTK3 и GDK3, они говорят, чтобы избежать этого с помощью
gdk_threads_add_idle()
или же
gdk_threads_add_timeout()
Но как мне это сделать, если я хочу, чтобы обновление выполнялось только после нажатия кнопки "Пуск"? Есть ли пример? Я не спрашиваю, как использовать gdk_threads_add_idle()
, Я спрашиваю, как запустить рабочую функцию в основном без потока после нажатия кнопки запуска.
Нажатие кнопки -> запустить рабочую функцию "в потоке ранее" -> обновить большое количество элементов GUI в окне GUI.
4 ответа
У вас есть 3 способа сделать это:
сделать вычисление в обратном вызове кнопки и использовать
gtk_event_pending()
/gtk_main_iteration()
использование
g_idle_add()
или другие, иgtk_event_pending()
/gtk_main_iteration()
использовать поток, в конечном итоге мьютекс, и
g_idle_add()
или другие Как правило, мьютекс не нужен, но он может решить некоторые ошибки или ошибки Heisenbugs.
Третье решение кажется наилучшим, потому что с первыми двумя методами у меня возникли некоторые проблемы при выходе из приложения во время выполнения вычислений. Приложение не закрывалось и печатало много предупреждений "Gtk Critical". (Я попробовал это на Windows и Mingw32).
1. кнопка обратного вызова:
Если вы хотите запустить рабочий поток в главном цикле gtk, вы можете напрямую выполнить вычисления в обратном вызове кнопки, обновив графический интерфейс и обработав события из него с помощью 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 ():
Вы также можете использовать вместо g_thread_new()
, gdk_thread_add_idle()
(в случае, если некоторые библиотеки не под вашим контролем могут использовать gdk_threads_enter()/leave()
) или же g_idle_add()
или же g_main_context_invoke()
:
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()
из рабочего потока может потребоваться заблокировать доступ к графическому интерфейсу с помощью мьютекса, поскольку между функциями, обращающимися к графическому интерфейсу, может возникнуть конфликт. Мьютекс должен быть инициализирован с 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);
}
Если вам нужно, чтобы функции, обновляющие графический интерфейс, выполнялись в определенном порядке, вам нужно добавить два счетчика и назначить номер для каждой функции, вызываемой с помощью 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);
}
Я также проверил случай блокировки отдельных виджетов вместо всего графического интерфейса, и это, кажется, работает.
В документации сказано, что вы все еще можете запускать свою рабочую функцию в потоке, вы просто не можете использовать функции GTK и GDK из этого потока. Таким образом, вы все равно можете начать поток, когда вы нажмете кнопку "Пуск". Но вместо обновления элементов графического интерфейса из потока вы должны запланировать их обновление из основного потока с помощью gdk_threads_add_idle()
,
Итак, ваша диаграмма должна выглядеть примерно так:
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();
}
Это старый вопрос, но я решил пойти дальше и добавить лучший способ сделать это:
Во-первых, проблема с методами Бертрана 1 и 2 заключается в том, что не рекомендуется создавать длительные потоки в потоке пользовательского интерфейса, даже если кто-то вызывает ожидающие обслуживания события, как можно видеть, затем он попадает в угловые случаи, включающие события закрытия и удаления, и, кроме того, это может привести к переполнению стека, если сделать это со слишком большим количеством виджетов, которые все делают это с длительной работой, и это просто кажется хрупким решением для вызова пластыряgtk_events_pending()
иgtk_main_iteration()
, это может работать для более коротких операций, когда нужно поддерживать работоспособность пользовательского интерфейса, делая что-то очень быстро, но для длительной сетевой операции это не кажется хорошим шаблоном проектирования, было бы гораздо лучше поместить это в это собственный поток, полностью отдельный от пользовательского интерфейса.
Теперь, если кто-то хочет обновить пользовательский интерфейс из такого длительного потока, например, выполнить несколько сетевых передач и сообщить о состоянии, тогда можно использовать каналы для межпоточного взаимодействия. Проблема с использованием мьютексов, подобных методу Бертрана 3, заключается в том, что получение блокировки может быть медленным и может блокироваться, если длительный поток уже получил блокировку, особенно способ, которым Бертран возвращается кdebutLoop
, это приводит к остановке потока пользовательского интерфейса в ожидании вычислительного потока, что неприемлемо.
Однако, используя каналы, можно взаимодействовать с потоком пользовательского интерфейса неблокирующим способом.
По сути, в начале программы создается неблокирующий канал из файла FIFO, а затем можно использоватьgdk_threads_add_idle
Чтобы создать дозорный поток для получения сообщений из потока в фоновом режиме, эта дозорная функция может даже существовать в течение всего времени существования программы, если, например, у вас есть потоки таймера, которые часто проверяют URL-адрес, чтобы обновить элемент пользовательского интерфейса с помощью его результата из HTTP. сделка.
Например:
/* 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;
}
Я получаю эту ошибку при закрытии главного окна: Gtk-CRITICAL **: gtk_widget_get_parent: утверждение 'GTK_IS_WIDGET (widget)' не выполнено
Я думаю, что нашел решение, используя две глобальные переменные, которые указывают на обратный вызов, чтобы остановить и вызвать gtk_main_quit()
и перехватил сигнал "уничтожить" для главного окна в самоопределяемый обратный вызов с именем 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" в главном окне.