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();
      }
   }
}

 

image

这种方法使得被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的代码也不会执行。

image

// @ 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代码的一大特点,代码可能比较多,但是经过自己精心维护却能对每一步的道理都能了解清楚。

image

// @ 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_MT_1.7z

GTK_MT_2.7z

GTK_MT_3.7z

注意,编译时假设你的gtk目录在c:\gtk\,gcc(MinGW)在C:\MinGW\。若不是,请自行修改编译和运行脚本的路径。

若不清楚gtk是怎么安装的,参考我的博客《Windows平台下Glade+GTK开发环境的搭建》进行安装。

posted on 2013-04-22 20:38  apollius  阅读(1472)  评论(0编辑  收藏  举报

导航