寫一個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等等暫時不做討論。下面圍繞這幾個函數和大家一起來看一些問題:
-
g_timeout_add的定時任務和gdk_threads_add_idle任務到底會不會影響主線程的操作。
-
g_thread_new對於線程安全的考慮
-
如何創建一個線程執行任務,但是主界面線程卻需要無卡死的等待(使用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這個函數其實可以保護刷新界面,卻不能保護全局變量的線程安全,相互操作得到結果也是亂七八糟。
其實我也挺納悶的,按照我的想法,這裏應該是線程安全的,但是無奈怎麼測試都是不對的數字不知道這邊有沒有什麼問題,先不管了後面在做考證吧。
(此處已經進行考證,是線程安全的:https://blog.csdn.net/FlayHigherGT/article/details/97379955)
附:不知道大家有沒有操作過下面這個信號:
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系統必然奔潰。
下面的操作可以參考下一篇系列博客:
對於gtk多線程編程的一些思考以及實踐歸納系列(2)