1. 程式人生 > >對於gtk多執行緒程式設計的一些思考以及實踐歸納

對於gtk多執行緒程式設計的一些思考以及實踐歸納

寫一個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系統必然奔潰。

這裡具體的程式碼操作等我再更新一個例子和大家分享吧~