对于gtk多线程编程的一些思考以及实践归纳

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/FlayHigherGT/article/details/84932747

写一个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等等暂时不做讨论。下面围绕这几个函数和大家一起来看一些问题:

  1. g_timeout_add的定时任务和gdk_threads_add_idle任务到底会不会影响主线程的操作。

  2. g_thread_new对于线程安全的考虑

  3. 如何创建一个线程执行任务,但是主界面线程却需要无卡死的等待(使用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系统必然奔溃。

这里具体的代码操作等我再更新一个例子和大家分享吧~

猜你喜欢

转载自blog.csdn.net/FlayHigherGT/article/details/84932747
今日推荐