写一个gtk的界面很久了,因为慢慢的在改良我的软件,所以也开始发现一些棘手的问题,当然,我这边指的问题只是gtk线程方面的问题,或者说如何才能执行一个界面以外的任务而使得界面不卡死,这样的任务包括多种多样,我这边有一些完成的方式,还有一些还没实现的,请大家听我一一道来。
首先我给大家列举几个gtk中最常见的这方面的函数:
g_timeout_add,g_timeout_add_seconds
g_thread_new,g_thread_join
g_idle_add,gdk_threads_add_idle
gdk_threads_enter,gdk_threads_leave
当然本人研究范围有限,只是和大家探讨部分,有些拓展函数类似gdk_threads_init g_timeout_add_timeout gdk_threads_add_idle_full等等暂时不做讨论。下面围绕这几个函数和大家一起来看一些问题:
-
g_timeout_add的定时任务和gdk_threads_add_idle任务到底会不会影响主线程的操作。
-
g_thread_new对于线程安全的考虑
-
如何创建一个线程执行任务,但是主界面线程却需要无卡死的等待(使用GtkSpinner转圈)线程任务的返回结果。
1、g_timeout_add的定时任务和gdk_threads_add_idle任务到底会不会影响主线程的操作。
这个问题其实很简单,一试便知,直接上demo代码:
#include <gtk/gtk.h>
#define TIME 2000000
gboolean task(gpointer data)
{
g_usleep(TIME);
g_print("callback task:Hello again-%s was pressed\n", (gchar*)data);
return FALSE;
}
gboolean timeout_task(gpointer data)
{
g_usleep(TIME);
g_print("callback timeout_task:Hello again-%s was pressed\n", (gchar*)data);
/*如果说return TRUE他会一直调用这个*/
return FALSE;
}
/*改进的回调函数,传递到该函数的数据将会被打印到标准输出*/
void callback(GtkWidget *widget, gpointer data)
{
gdk_threads_add_idle((GSourceFunc)task, data);
//g_timeout_add(1, (GSourceFunc)timeout_task, data);
g_print("after task\n");
}
/*关闭窗口的函数*/
void destroy(GtkWidget *widget, gpointer data)
{
g_print("退出hello world!\n");
gtk_main_quit();
}
int main(int argc, char *argv[])
{
GtkWidget *window;
GtkWidget *button;
GtkWidget *box;
GtkWidget *spinner;
/*函数gtk_init()会在每个GTK的应用程序中调用。
* 该函数设定默认的视频和颜色默认参数,接下来会调用函数
* gdk_init()该函数初始化要使用的库,设定默认的信号处理
*检查传递到程序的命令行参数
* */
gtk_init(&argc, &argv);
//下面两行创建并显示窗口。创建一个200*200的窗口。
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
/*设置窗口标题*/
gtk_window_set_title(GTK_WINDOW(window), "Helloworld.c test!");
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ALWAYS);//居中
g_signal_connect(G_OBJECT(window), "delete_event", G_CALLBACK(destroy), NULL);
/*设置窗口边框的宽度*/
gtk_container_set_border_width(GTK_CONTAINER(window), 80);
/*创建一个组装盒
*我们看不见它,用来排列构建的工具
* */
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
/*把组装盒box1放到主窗口中*/
gtk_container_add(GTK_CONTAINER(window), box);
/*打开spinner等待按钮*/
spinner = gtk_spinner_new();
gtk_spinner_start(GTK_SPINNER(spinner));
gtk_box_pack_start(GTK_BOX(box), spinner, TRUE, TRUE, 0);
gtk_widget_show(spinner);
/*创建一个标签为“欢迎”的按钮*/
button = gtk_button_new_with_label("欢迎");
/*当按下欢迎按钮时,我们调用 callback函数,会打印出我们传递的参数*/
g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(callback), "欢迎大家来到我的博客学习!");
/*我们将button 按钮放入组装盒中*/
gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);
/*欢迎按钮设置成功,别忘了写下个函数来显示它*/
gtk_widget_show(button);
/*创建第二个按钮*/
button = gtk_button_new_with_label("说明");
g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(callback), "GTK编程入门学习!");
gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);
gtk_widget_show(button);
/*创建一个退出按钮*/
button = gtk_button_new_with_label("退出");
/*当点击退出按钮时,会触发gtk_widet_destroy来关闭窗口,destroy信号从这里发出
* 会触发destroy函数。*/
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL);
g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), window);
gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);
gtk_widget_show(button);
gtk_widget_show(box);
gtk_widget_show(window);
//进入主循环
gtk_main();
return 0;
}
这段代码从网上随便截取了一个界面小例子,打开程序显示三个按钮以及一个spinner旋转等待按钮,之后我们的测试用例都用这个基础界面。显示结果显而易见,"after task"直接先被打印,之后sleep两秒,会显示下面"callback task:Hello again-"等字样,而且在sleep期间,spinner旋转按钮是不会动的,所以结论很明显:
g_timeout_add的定时任务和gdk_threads_add_idle任务是肯定会影响主线程操作的。
2、g_thread_new对于线程安全的考虑
第二点个人感觉还是比较重要的,尤其是这点和gdk_threads_add_idle这个函数关系很大。我们都知道,在ui编程中,如果你开创了一个线程执行一些任务,你是万万不能在线程中对ui线程中的东西进行操作的,这样会导致系统奔溃。还有就是主线程的全局变量,你要在线程中操作这些全局变量就必须要考虑到线程安全。因此gtk官方给出了一个好的接口gdk_threads_add_idle,其实这个接口的前身就是gdk_threads_enter和gdk_threads_leave,不过这两个接口已经被遗弃了,更新为gdk_threads_add_idle,网上有很多例子是关于以前的两个函数的,先说说老版的接口:
https://www.cnblogs.com/cappuccino/p/5987738.html这是一个相关例子,因为老版不再用就不多说了。
主要步骤解析:
1、g_thread_init目的是要让这个GObject的动态系统支持多线程,在GTK+2.24.10以后的版本中默认就已经支持多线程系统,不再需要调用这个函数了。
2、gdk_threads_init 这个函数是用来初始化GTK+在多线程时使用的全局锁,所以必须放在gtk_init之前。
3、gtk_main必须被gdk_threads_enter和gdk_threads_leave包裹,那么何时调用gdk_threads_enter取决与你的线程何时启动何时需要UI同步,举例说明一下,如果你启动了一个线程很早就需要同步对GUI进行刷新,那么你就要在你调用线程的刷新之前调用它。
老版的函数很显然就是将需要在线程中操作到ui的代码用gdk_threads_enter和gdk_threads_leave包裹起来就能做到线程安全了
gdk_threads_add_idle这个函数为什么能完美的代替上面的接口呢?我们来看一个线程安全的例子:
#include <gtk/gtk.h>
static GtkWidget *btn1;
static GtkWidget *btn2;
gboolean task2(gpointer data)
{
gtk_button_set_label((GtkButton *)btn1, "not main");
return FALSE;
}
gboolean thread_task(gpointer data)//多线程解决
{
gdk_threads_add_idle((GSourceFunc)task2, data);
/*如果说return TRUE他会一直调用这个*/
return FALSE;
}
/*改进的回调函数,传递到该函数的数据将会被打印到标准输出*/
void callback(GtkWidget *widget, gpointer data)
{
gtk_button_set_label((GtkButton *)btn1, "main");
}
void callback2(GtkWidget *widget, gpointer data)
{
g_thread_new(NULL, (GThreadFunc)thread_task, data);
}
/*关闭窗口的函数*/
void destroy(GtkWidget *widget, gpointer data)
{
g_print("退出hello world!\n");
gtk_main_quit();
}
int main(int argc, char *argv[])
{
GtkWidget *window;
GtkWidget *button;
GtkWidget *box;
/*函数gtk_init()会在每个GTK的应用程序中调用。
* 该函数设定默认的视频和颜色默认参数,接下来会调用函数
* gdk_init()该函数初始化要使用的库,设定默认的信号处理
*检查传递到程序的命令行参数
* */
gtk_init(&argc, &argv);
//g_mutex = g_mutex_new();
//下面两行创建并显示窗口。创建一个200*200的窗口。
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
/*设置窗口标题*/
gtk_window_set_title(GTK_WINDOW(window), "Helloworld.c test!");
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER_ALWAYS);//居中
g_signal_connect(G_OBJECT(window), "delete_event", G_CALLBACK(destroy), NULL);
/*设置窗口边框的宽度*/
gtk_container_set_border_width(GTK_CONTAINER(window), 80);
/*创建一个组装盒
*我们看不见它,用来排列构建的工具
* */
box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
/*把组装盒box1放到主窗口中*/
gtk_container_add(GTK_CONTAINER(window), box);
/*等待按钮*/
spinner = gtk_spinner_new();
gtk_spinner_start(GTK_SPINNER(spinner));
gtk_box_pack_start(GTK_BOX(box), spinner, TRUE, TRUE, 0);
gtk_widget_show(spinner);
/*创建一个标签为“欢迎”的按钮*/
btn1 = gtk_button_new_with_label("主线程");
/*当按下欢迎按钮时,我们调用 callback函数,会打印出我们传递的参数*/
g_signal_connect(G_OBJECT(btn1), "clicked", G_CALLBACK(callback), "欢迎大家来到我的博客学习!");
/*我们将button 按钮放入组装盒中*/
gtk_box_pack_start(GTK_BOX(box), btn1, TRUE, TRUE, 0);
/*欢迎按钮设置成功,别忘了写下个函数来显示它*/
gtk_widget_show(btn1);
/*创建第二个按钮*/
btn2 = gtk_button_new_with_label("分线程");
g_signal_connect(G_OBJECT(btn2), "clicked", G_CALLBACK(callback2), "GTK编程入门学习!");
gtk_box_pack_start(GTK_BOX(box), btn2, TRUE, TRUE, 0);
gtk_widget_show(btn2);
/*创建一个退出按钮*/
button = gtk_button_new_with_label("退出");
/*当点击退出按钮时,会触发gtk_widet_destroy来关闭窗口,destroy信号从这里发出
* 会触发destroy函数。*/
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL)
g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), window);
gtk_box_pack_start(GTK_BOX(box), button, TRUE, TRUE, 0);
gtk_widget_show(button);
gtk_widget_show(box);
gtk_widget_show(window);
//进入主循环
gtk_main();
return 0;
}
callback是主线程按钮回调,callback2是创建了一个线程,两个按钮都是修改ui上的某一个按钮的文字,在callback2中如果直接调用gtk_button_set_label((GtkButton *)btn1, "not main");修改按钮的label,那么系统即将奔溃,因为线程中是不允许去刷新ui的,但是在线程中执行gtk_threads_add_idle去刷新ui那就ok啦。所以gtk_threads_add_idle代替之前的enter和leave的方法也就是实现了如何在线程中刷新界面,他其实是在主线程中执行了这句刷新ui的程序,而且他会等待主线程空闲的时候去调用,保证不会与主线程冲突。他刷新界面其实就是一个低优先级的任务,在主线程没有需要做的事情的时候,他就会去刷新一下,这就是用这个函数的意义。
问题:
如果在gtk_threads_add_idle中操作全局变量,会和主线程中冲突吗?这个问题我们再来做一个实验。
static int g_num = 0;
gboolean task2(gpointer data)
{
//g_num++;
return FALSE;
}
gboolean thread_task(gpointer data)//多线程解决
{
for(int i = 0; i < 10000; i++)
{
g_usleep(100);
//gdk_threads_add_idle((GSourceFunc)task2, data);
//g_mutex_lock(g_mutex);
g_num++;
//g_mutex_unlock(g_mutex);
}
g_print("++g_num=%d\n", g_num);
/*如果说return TRUE他会一直调用这个*/
return FALSE;
}
/*改进的回调函数,传递到该函数的数据将会被打印到标准输出*/
void callback(GtkWidget *widget, gpointer data)
{
for(int i = 0; i < 10000; i++)
{
g_usleep(100);
//g_mutex_lock(g_mutex);
g_num--;
//g_mutex_unlock(g_mutex);
}
g_print("--g_num=%d\n", g_num);
}
void callback2(GtkWidget *widget, gpointer data)
{
g_thread_new(NULL, (GThreadFunc)thread_task, data);
}
这里我们没有给出主函数,主函数大致和上面一样,有两个按钮,一个按钮执行主线程任务,一个按钮创建新线程执行任务,主线程中我们将全局变量加加10000,新线程我们将全局变量减减10000,如果线程安全的话我们得到的结果肯定是0,现在有三种情况:
1、都不加锁,主副线程同时操作。
2、都不加锁,副线程在gdk_threads_add_idle里面操作。
3、都加锁。
第一种情况毋庸置疑,得到结果是乱七八糟每次都不一样,因为两个线程同时操作一个全局变量不加锁是万万不可的,第三种情况,肯定是正确的。但是第二种情况测试下来,竟然也是失败的,说明gdk_threads_add_idle这个函数其实可以保护刷新界面,却不能保护全局变量的线程安全,相互操作得到结果也是乱七八糟。
其实我也挺纳闷的,按照我的想法,这里应该是线程安全的,但是无奈怎么测试都是不对的数字不知道这边有没有什么问题,先不管了后面在做考证吧。
附:不知道大家有没有操作过下面这个信号:
g_signal_connect(G_OBJECT(g_snotebook), "switch-page", G_CALLBACK(change_pic), NULL);
这是GtkNotebook的一个切换信号,当我们切换到另外一个页面的时候,如果你这个页面有很多请求的操作,界面是会卡死知道等待你将操作结束,之后才会翻页,相当于按钮卡死一会,这用户体验也极差,一次我们可以将任务翻页后的函数放到gdk_threads_add_idle里面这样翻页操作会立马完成,只不过你请求的数据会过一会才请求到,但是这样的话用户体验会好很多。
3、如何创建一个线程执行任务,但是主界面线程却需要无卡死的等待(使用GtkSpinner转圈)线程任务的返回结果。
在我刚开始写程序不久的时候,我考虑这个问题很简单,比如说我要实现一个功能是这样的:我在linux c下调用了一系列系统命令用fork+exec,开始执行的时候我讲gtk_spinner_start一下,让界面显示正在执行任务,当然,命令执行下去了,接下来的时间就是要等待结果,执行命令我是用子进程做的不会影响负进程,但是你是如何知道命令执行完毕了呢??所以我就想了一个 g_timeout_add任务去刷每个两秒刷新一次,任务里面去popen读取本地的某些变量是否完成了这次命令,但是很难受的是,每次读取本地变量判断是否成功完成命令行还是超时的时候,都会因为时间太长而卡主主界面,所以gtkspinner每个两秒钟就会停止转动一下,因为我们在检验这条指令是否完成,虽然可以实现功能,但是界面上很糙。这个问题我写个伪代码来演示一下:
gboolean timeout_task()
{
g_usleep(TIME);//睡半秒表示我们在验证命令行是否完成
/*如果说return TRUE他会一直调用这个*/
return FALSE;
}
gboolean callback()//按钮的callback
{
g_add_timeout_seconds(1, (GSourceFunc)timeout_task, NULL);//执行任务每隔一秒验证一下
}
int main
{
显示一个按钮和一个spinner正在转动
}
很显然,主界面的spinner按钮会每隔一秒停止转动一下,看这很难受。
所以后来想到了一个新的办法,下面给出我的思路:
1、首先我们在界面开始的时候创建一个线程以及一个全局队列,while循环不断判断队列是否有数据。
2、在我们需要网络请求的时候,将网络请求需要的参数,请求完之后需要执行的函数指针包裹在队列数据结构里面,入队。
3、线程得到队列里面的任务,进行网络请求,这个过程可能会延续好几秒钟,但是没事这是在线程中完成的,完成请求之后回调那个传进来的函数指针,即可完成相应的反馈。
4、回调函数肯定有刷新界面等操作,因此这个回调函数必须要在gdk_threads_add_idle中完成,不然在线程中操作ui系统必然奔溃。
这里具体的代码操作等我再更新一个例子和大家分享吧~