zoukankan      html  css  js  c++  java
  • 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开发环境的搭建》进行安装。

  • 相关阅读:
    8.16
    8.6 总结
    Educational Codeforces Round 45 (Rated for Div. 2)
    Codeforces Round #487 (Div. 2)
    Codeforces Round #485
    codeforces Avito Code Challenge 2018
    MySQL索引知识面试题
    使用多线程优化复杂逻辑以及数据量多处理
    elasticsearch 和Ik 以及 logstash 同步数据库数据
    linux 安装elasticsearch步骤以及入的坑
  • 原文地址:https://www.cnblogs.com/apollius/p/3036325.html
Copyright © 2011-2022 走看看