Windows平台下Glade+GTK实现多线程界面的探讨
【@.1 简介】
如果是做底层嵌入式开发的人,对于函数的可重入性在多任务系统下的理解应该是比较深刻的,如果不理解清楚任务是如何调度的,任务之间的通讯机制是怎样的,那之后的系统设计就无从下手。如果是经常做上层开发的人,可能不需要去搞清楚界面的主函数里到底干了什么,也不需要搞懂开始多线程之后对函数本身的可重入性有什么要求。所以我在着手写了上一篇博客《Windows平台下Glade+GTK开发环境的搭建》之后,第一件事情就是设计一个涉及到多任务(多线程)的界面,从中分析GTK的消息循环和任务调度机制。
还不清楚GTK和Glade的使用的人可以参考我的上一篇博客,这里不再累述。
多线程在界面设计里是非常重要的,比如,可以新开一个线程单独进行逻辑处理、IO读取,主线程用于界面显示,这样就不会出现因为逻辑处理时所堵塞导致界面死掉。但是,一旦开始多线程,最需要关心的一点就是数据的保护。一般情况下,不允许多个线程在同一时刻对同一个资源进行读写(比如一个全局变量,一个界面标签上的text),否则会照成无法预计的冲突,特别是在界面更新这一点上。若使用windows的.Net进行开发的话可以使用BeginInvoke方法实现进行异步调度,其中封装了类似于互斥信号量的机制来保护共享控件(这里是界面)。windows下直接不允许其他线程修改界面更新线程,即主线程。但是gtk下是可以的,因为,它不会添加线程保护代码。这样就导致安全性下降,而且实际上在别的线程进行界面修改确实会产生很多问题,所以我们得自己添加代码进行保护,这就涉及到多线程之间的调度问题。
考虑下面一个问题:设计一个界面包含三个按钮和一个label,点击一个按钮可以递增label上的数,点击另一个可以清零,点击最后一个可以自动开启一个计时器进行递增,将递增的结果显示在界面上,并且递增的速度能在程序中修改。前两个按钮很好设计,最后一个按钮我们采用新建一个线程的方法进行处理。所有源代码在最后都有打包下载。
【方法一:粗暴的多线程保护】
首先需要说明的是,gtk中不支持线程的直接挂起(suspend)和恢复(resume),也不支持在程序中直接销毁一个线程。若使用线程池的话或许能从侧面解决这一问题,这有待验证。在GLib2.32之后的版本中由函数g_thread_unref()可以实现销毁线程,但是目前官网上window下的glib移植只到2.24。等我有时间研究其linux下的源码再来解决这一问题,所以目前就先在这些限制下来进行设计,即,线程都是创建之后立刻运行,等整个程序结束后统一销毁。
方法一,也是最开始看gtk库时能想到的。使用gdk_threads_enter()和gdk_threads_leave()包围线程中的代码,部分代码如下。
int main (int argc, char *argv[]) { //Thread aware if (!g_thread_supported ()){ g_thread_init(NULL); } gdk_threads_init(); gtk_init(&argc, &argv); // ... Other initials gdk_threads_enter(); gtk_main(); gdk_threads_leave(); return 0; } // ... static void TimerThread(void * pdata) { while(1) { g_usleep(100000); //100 ms if(flagThread == 1) //Check if it can do the follow { gdk_threads_enter(); //Suspended here whatever you do, if the main thread is doing something labelCount++; labelPrint(labelCount); gdk_threads_leave(); } } }
这种方法使得被gdk_threads_enter()和gdk_threads_leave()包围的代码在线程上无法同时运行,而且一般主循环gtk_main()被包围,所以只要其他线程进入到被包围的区间,主循环此时必然暂停,等其他线程结束,或是向前面代码一样进入sleep状态后,才能继续运行!
对于不清楚函数重入性或没做过嵌入式系统开发的人,我再简单解释下为什么线程里会有一个死循环。任何任务都会有一个死循环,否则任务将会返回,无法第二次执行。但是死循环不能让他一直运行,否则将会一直占用CPU的使用权。所以像前面TimerThread里面每次循环都会加一句g_sleep()进行延时,此时它通知系统任务空闲,将被任务调度器切换到别的需要CPU的任务。如果这里不加系统延时,则将一直在这个函数里循环,不能回到主函数。所以不能自己写个简单的for循环实现的延时函数,这样是不能释放CPU使用权的。当然还有其它系统函数也能起到释放CPU使用权的作用,比如等待信号量、队列的函数等等,这里先暂时不讨论。
前面的代码中TimerThread里有个判断if(flagThread == 1),flagThread是个全局变量,将在界面上Start按键按下去之后设为1,即线程往下做,否则线程不能进入被if包围的语句。注意,这并不意味着线程被挂起,线程还是每100ms检查一次flagThread的状态的。这也是对于没有能直接挂起、恢复线程的gtk的线程库的解决方法,就是拿扫描代替中断。
最后,这种方法的确可以用新的线程进行界面处理,但这并不是一种很好的多线程方法。你可以下载文末的压缩包,运行这个编译,用鼠标拖动主界面就知道问题了。这时候标签的递增将会停止,界面依旧呈现假死状态。因为你拖动主界面时,程序切换到主循环中,这时线程TimerThread是无法进入的,所以标签上的递增数无法更新,不管你在这个线程中做了什么,即使没有对共享资源进行操作,都将无法进行下去。
【方法二:在主循环中进行标签刷新】
在主循环中插入一段函数,专门用于更新标签信息。这样,在线程TimerThread中仅仅对将要被更新的计数器全局变量labCnt进行递增,并且这段操作用互斥量函数g_mutex_lock()和g_mutex_unlock()包围,同理,主函数中涉及到对labCnt递增的操作也用互斥量进行包围,这样一来,仅仅在操作labCnt的很短期间会出现无法同时处理的现象,其他时候两个线程没有交集,不会引起卡死现象。当然,这在gtk的实现中是,如下面代码所示,利用一个GSource资源,为了达到最有效果,我在点击第三个按钮开始计数时才开始新建这个GSource资源timeoutSrc,设定每隔30ms执行一次(基本上人眼够用了),而需要停止时销毁这个资源。这个资源也在每次新建时绑定到函数TimeOutFunc,这个函数内部仅仅将labCnt的值打印到主界面上。将这个资源用g_source_attach()添加到主循环里,这样主循环每次扫描时将执行这个资源的函数,即TimeOutFunc进行界面上的label更新。这时在主界面上才进行更新操作,并且若没有开启新的线程计数,这段刷新label的代码也不会执行。
// @ author . apollius // @ date . Apr 22, 2013 // @ brief . Multi-thread to refresh a Label #include <gtk/gtk.h> //#include <glib.h> #include <glib/gprintf.h> //Add G_MODULE_EXPORT to signal function prototype is important in Windows!!! //#define G_MODULE_EXPORT __declspec(dllexport) GtkWidget *win_main; GtkLabel *lab_Show; unsigned int labCnt; GMutex *labCntMutex; char labelbuffer[10]; GThread *timerThr; GMutex *thrSwiMutex; unsigned char thrSwi; //1 for start, 0 for stop GSource *timeoutSrc; // static void TimerThread (void * pdata); static void labCntPrint (); static void TimeOutFunc (void* data); // //Thread enter is a rude idea for mutex communication //Drag the window around and see if the cmd still prints. /*Use mutex instead of gdk_threads_enter(), to protext variables*/ /*This will do better */ static void TimerThread(void * pdata) { while(1) { if(thrSwi == 0) { g_usleep(300000); //300 ms for start stop check g_printf("***TimerThread not run***\n"); } else { g_usleep(100000); //100 ms //gdk_threads_enter(); //Suspended here whatever you do, if the main thread is doing something g_mutex_lock(labCntMutex); labCnt++; g_mutex_unlock(labCntMutex); //gdk_threads_leave(); g_printf("***TimerThread running***\n"); } } } /* Add to timeout in mainContex's each loop, do label print*/ /* Use this method to fresh labCnt changed in TimerThread*/ static void TimeOutFunc(void* data) { g_printf("***Time Out***\n"); labCntPrint(); } /* Read labCnt and print. Must set a mutex to protect labCnt */ static void labCntPrint() { int i; g_mutex_lock(labCntMutex); g_sprintf(labelbuffer,"%d",labCnt); g_mutex_unlock(labCntMutex); gtk_label_set_text(lab_Show, labelbuffer); //clear for(i=0;i<10;i++) labelbuffer[i]=0; } G_MODULE_EXPORT void on_btn_Click_clicked(GtkObject *object, gpointer user_data) { g_printf("btn_Click Pressed!\n"); g_mutex_lock(labCntMutex); labCnt++; g_mutex_unlock(labCntMutex); labCntPrint(); } G_MODULE_EXPORT void on_btn_Clear_clicked(GtkObject *object, gpointer user_data) { g_printf("Cleard!\n"); g_mutex_lock(labCntMutex); labCnt=0; g_mutex_unlock(labCntMutex); labCntPrint(); } G_MODULE_EXPORT void on_tgb_Timer_toggled(GtkObject *object, gpointer user_data) { gboolean state = gtk_toggle_button_get_active((GtkToggleButton *)object); if(state == TRUE) { g_mutex_lock (thrSwiMutex); thrSwi = 1; //Start Thread //g_source_set_callback(timeoutSrc, (GSourceFunc)TimeOutFunc, (void*)0, NULL);// g_mutex_unlock (thrSwiMutex); timeoutSrc = g_timeout_source_new(30); //30ms g_source_set_callback(timeoutSrc, (GSourceFunc)TimeOutFunc, (void*)0, NULL);// g_source_attach(timeoutSrc, NULL); //add to the main GMainContex g_print("Pressed\n"); gtk_button_set_label((GtkButton *)object, "Stop"); } else { g_mutex_lock (thrSwiMutex); thrSwi = 0; //Stop Thread //g_source_set_callback(timeoutSrc, (GSourceFunc)TimeOutFuncVoid, (void*)0, NULL);// g_mutex_unlock (thrSwiMutex); if(!g_source_is_destroyed(timeoutSrc)) { g_source_destroy(timeoutSrc); } g_print("Deressed\n"); gtk_button_set_label((GtkButton *)object, "Start"); } } G_MODULE_EXPORT void on_win_Main_destroy(GtkObject *object, gpointer user_data) { //If you comment the follow, you will leave the cmd window alive when close the main window we build. gtk_main_quit(); } int main (int argc, char *argv[]) { GtkBuilder *builder; int i; //Thread aware if (!g_thread_supported ()){ g_thread_init(NULL); } gdk_threads_init(); // gtk_init(&argc, &argv); builder = gtk_builder_new(); gtk_builder_add_from_file(builder, "Tutor3.glade", NULL); // timerThr = g_thread_create((GThreadFunc)TimerThread, (void *)0, FALSE, NULL ); thrSwi = 0; thrSwiMutex = g_mutex_new(); // Fetch some widgets created in glade win_main = GTK_WIDGET(gtk_builder_get_object(builder, "win_Main")); lab_Show = GTK_LABEL (gtk_builder_get_object(builder, "lab_Show")); // for(i=0;i<10;i++) labelbuffer[i]=0; labCnt = 0; labCntMutex = g_mutex_new(); labCntPrint(); g_printf("%d",labCnt); gtk_builder_connect_signals(builder, NULL); g_object_unref(G_OBJECT(builder)); gtk_widget_show(win_main); //Thread aware gdk_threads_enter(); /*********************** Enter Main Thread *******************************/ gtk_main(); /*********************** Exit Main Thread ********************************/ gdk_threads_leave(); return 0; }
【方法三:利用定时器进行更精准的计数器递增】
这种方法其实仅仅是一个对于计数器递增时的时间精度的改进。同时,由于增加了计数器,多种任务间的通讯机制将导致系统更复杂。其实这不复杂,虽然对于做惯了.Net的人来说,像这种定时器的问题简单的几行代码就好了,但是鉴于gtk还是一种比较纯净的C代码来实现的,所以很多工作需要我们自己添加。
如图所示,点击第三个按钮时,将会新建一个GSource,timeoutSrc用于连接一个更新label的函数,并插入到主循环中每次主循环扫描时执行。新建一个GTimer类型的ticker,用于技术。由于gtk中没有一个很好的定时中断机制让定时器自动产生中断,所以还是得我们自己编写代码查询。(其实只要查询速度够快,跟中断机制的效果差不多的)。对于定时器ticker的查询我为了区别前面的线程操作,故意在主循环中,而且是Idle空闲任务中插入查询函数IdleTickerScan进行定时器的查询,若到达一定的定时时间,将利用消息对列发送一则消息g_async_queue_push();与此同时TimerThread一开始被建立,但是一直卡在g_async_queue_pop()处,没有消息是无法继续进行的。一旦IdleTickerScan中发送一则消息,TimerThread将会收到消息,并清空这则消息,往下执行直到下个循环继续等待。
注意到这里,IdleTickerScan是加载主循环的Idle任务中的。一般每次主循环中的Idle任务都是一个空循环,而且循环次数不止一次,所以IdleTickerScan每次主循环将不止执行一次,而且这里不能放太多的代码降低系统效率。有人将多线程中更新主界面的功能放在Idle里,其实这样是没有必要的,而且将有可能导致空闲任务反而任务繁重。更新界面可以就像我这样新建一个插在主循环中每隔30ms更新一次就可以了。
整个系统设计下来感觉比较复杂,主要是自己需要些的代码比较多,这也正是C代码的一大特点,代码可能比较多,但是经过自己精心维护却能对每一步的道理都能了解清楚。
// @ author . apollius // @ date . Apr 22, 2013 // @ brief . Multi-thread to refresh a Label #include <gtk/gtk.h> //#include <glib.h> #include <glib/gprintf.h> //Add G_MODULE_EXPORT to signal function prototype is important in Windows!!! //#define G_MODULE_EXPORT __declspec(dllexport) GtkWidget *win_main; GtkLabel *lab_Show; unsigned int labCnt; GMutex *labCntMutex; char labelbuffer[10]; GThread *timerThr; GTimer *ticker; GAsyncQueue *tickerAsync; unsigned int idleID; unsigned int timeoutID; // static void TimerThread (void * pdata); static void labCntPrint (); static void TimeOutFunc (void * pdata); static void IdleTickerScan (void * pdata); // /*Use mutex instead of gdk_threads_enter(), to protext variables*/ /*This will do better */ static void TimerThread(void * pdata) { while(1) { //wait untill there has a data, which is pushed in idle task in main thread. //Usually this thread is faster than the main thread, so it will wait during loops g_async_queue_pop(tickerAsync); g_mutex_lock(labCntMutex); labCnt++; g_mutex_unlock(labCntMutex); } } /* Add to timeout in mainContex's each loop, do label print*/ /* Use this method to fresh labCnt changed in TimerThread*/ static void TimeOutFunc(void * pdata) { g_printf("***Time Out***\n"); labCntPrint(); } static void IdleTickerScan(void * pdata) { if(g_timer_elapsed(ticker, (gulong*)0)>0.05 ) //50 ms { g_async_queue_push(tickerAsync, (void*)1); //do not push NULL or void* 0 g_timer_start(ticker); //reset the ticker } } /* Read labCnt and print. Must set a mutex to protect labCnt */ static void labCntPrint() { int i; g_mutex_lock(labCntMutex); g_sprintf(labelbuffer,"%d",labCnt); g_mutex_unlock(labCntMutex); gtk_label_set_text(lab_Show, labelbuffer); //clear for(i=0;i<10;i++) labelbuffer[i]=0; } G_MODULE_EXPORT void on_btn_Click_clicked(GtkObject *object, gpointer user_data) { g_printf("btn_Click Pressed!\n"); g_mutex_lock(labCntMutex); labCnt++; g_mutex_unlock(labCntMutex); labCntPrint(); } G_MODULE_EXPORT void on_btn_Clear_clicked(GtkObject *object, gpointer user_data) { g_printf("Cleard!\n"); g_mutex_lock(labCntMutex); labCnt=0; g_mutex_unlock(labCntMutex); labCntPrint(); } G_MODULE_EXPORT void on_tgb_Timer_toggled(GtkObject *object, gpointer user_data) { gboolean state = gtk_toggle_button_get_active((GtkToggleButton *)object); if(state == TRUE) { ticker = g_timer_new();//This will start timer automatically idleID = g_idle_add((GSourceFunc)IdleTickerScan, (void*)0); timeoutID = g_timeout_add(30, (GSourceFunc)TimeOutFunc, (void*)0); g_print("Pressed\n"); gtk_button_set_label((GtkButton *)object, "Stop"); } else { g_timer_destroy(ticker); g_source_remove (idleID ); g_source_remove (timeoutID ); g_print("Deressed\n"); gtk_button_set_label((GtkButton *)object, "Start"); } } G_MODULE_EXPORT void on_win_Main_destroy(GtkObject *object, gpointer user_data) { //If you comment the follow, you will leave the cmd window alive when close the main window we build. //g_timer_destroy(ticker); g_printf("Destroyed\n"); gtk_main_quit(); } int main (int argc, char *argv[]) { GtkBuilder *builder; int i; //Thread aware if (!g_thread_supported ()){ g_thread_init(NULL); } gdk_threads_init(); // gtk_init(&argc, &argv); builder = gtk_builder_new(); gtk_builder_add_from_file(builder, "Tutor3.glade", NULL); // Fetch some widgets created in glade win_main = GTK_WIDGET(gtk_builder_get_object(builder, "win_Main")); lab_Show = GTK_LABEL (gtk_builder_get_object(builder, "lab_Show")); // for(i=0;i<10;i++) labelbuffer[i]=0; labCnt = 0; labCntMutex = g_mutex_new(); labCntPrint(); gtk_builder_connect_signals(builder, NULL); g_object_unref(G_OBJECT(builder)); gtk_widget_show(win_main); // tickerAsync = g_async_queue_new(); //This time, the thread must be create after the Asysncqueue, as it wait for the queue to continue timerThr = g_thread_create((GThreadFunc)TimerThread, (void *)0, FALSE, NULL ); //Thread aware gdk_threads_enter(); /*********************** Enter Main Thread *******************************/ gtk_main(); /*********************** Exit Main Thread ********************************/ gdk_threads_leave(); return 0; }
以上三种方法所有代码,以及编译脚本的下载:
注意,编译时假设你的gtk目录在c:\gtk\,gcc(MinGW)在C:\MinGW\。若不是,请自行修改编译和运行脚本的路径。
若不清楚gtk是怎么安装的,参考我的博客《Windows平台下Glade+GTK开发环境的搭建》进行安装。