如果您的程式只是一個單執行緒,單一流程的程式,那麼通常您只要注意到程式邏輯的正確,您的程式通常就可以正確的執行您想要的功能,但當您的程式是多執行緒程式,多流程同時執行時,那麼您就要注意到更多的細節,例如在多執行緒共用同一物件的資料時。
如果一個物件所持有的資料可以被多執行緒同時共享存取時,您必須考慮到「資料同步」的 問題,所謂資料同步指的是兩份資料的整體性一致,例如物件A有 name與id兩個屬性,而有一份A1資料有name與id的資料要更新物件A的屬性,如果A1的name與id設定給A物件完成,則稱A1與A同步,如 果A1資料在更新了物件的name屬性時,突然插入了一份A2資料更新了A物件的id屬性,則顯然的A1資料與A就不同步,A2資料與A也不同步。
資料在多執行緒下共享時,就容易因為同時多個執行緒可能更新同一個物件的資訊,而造成物件資料的不同步,因為資料的不同步而可能引發的錯誤通常不易察覺, 而且可能是在您程式執行了幾千幾萬次之後,才會發生錯誤,而這通常會發生在您的產品已經上線之後,甚至是程式已經執行了幾年之後。
這邊舉個簡單的例子:
- gmutex_demo.c
#include <glib.h>
struct _User {
GString *name;
GString *id;
glong count;
};
typedef struct _User User;
void user_set_name_id(User *user, GString *name, GString *id) {
user->name = name;
user->id = id;
if(!user_check_name_id(user)) {
g_print("%d: illegal name or id....\n", user->count);
}
user->count++;
}
gboolean user_check_name_id(User *user) {
return user->name->str[0] == user->id->str[0];
}
gpointer thread1(gpointer user) {
GString *name = g_string_new("Justin Lin");
GString *id = g_string_new("J.L.");
while(TRUE) {
user_set_name_id(user, name, id);
}
}
gpointer thread2(gpointer user) {
GString *name = g_string_new("Shang Hwang");
GString *id = g_string_new("S.H.");
while(TRUE) {
user_set_name_id(user, name, id);
}
}
int main(int argc, char *argv[]) {
GMainLoop *mloop;
if(!g_thread_supported()) {
g_thread_init(NULL);
}
User user;
mloop = g_main_loop_new(NULL, FALSE);
g_thread_create(thread1, &user, FALSE, NULL);
g_thread_create(thread2, &user, FALSE, NULL);
g_main_loop_run(mloop);
return 0;
}
在這個程式中,您可以設定使用者的名稱與縮寫id,並簡單檢查一下名稱與id的第一個字是否相同,單就這個程式本身而言,user_set_name_id()並沒有任何的錯誤,但如果它被 用於多執行緒的程式中,而且同一個物件被多個執行存取時,就會"有可能"發生錯誤,一個執行的可能結果如下(為簡化範例,並無設置停止條件,請直接Ctrl+C結束程式):
51307: illegal name or id....
94812: illegal name or id....
140423: illegal name or id....
174257: illegal name or id....
214260: illegal name or id....
214260: illegal name or id....
259266: illegal name or id....
314738: illegal name or id....
350144: illegal name or id....
402701: illegal name or id....
444026: illegal name or id....
481165: illegal name or id....
....
94812: illegal name or id....
140423: illegal name or id....
174257: illegal name or id....
214260: illegal name or id....
214260: illegal name or id....
259266: illegal name or id....
314738: illegal name or id....
350144: illegal name or id....
402701: illegal name or id....
444026: illegal name or id....
481165: illegal name or id....
....
看到了嗎?如果以單執行緒的觀點來看,上面的訊息在測試中根本不可能出現,然而在這個程式中卻出現了錯誤,而且重點是,第一次錯誤是發生在第51307次的設定(您的電腦上可能是不同的數字),如果您在程式完成並開始應用之後,這個時間點可能是幾個月甚至幾年之後。
問題出現哪?在於這邊:
void user_set_name_id(User *user, GString *name, GString *id) {
user->name = name;
user->id = id;
if(!user_check_name_id(user)) {
g_print("%d: illegal name or id....\n", user->count);
}
user->count++;
}
user->name = name;
user->id = id;
if(!user_check_name_id(user)) {
g_print("%d: illegal name or id....\n", user->count);
}
user->count++;
}
雖然您設定給它的參數並沒有問題,在某個時間點時,thread1設定了"Justin Lin", "J.L."給name與id,在進行測試的前一刻,thread2可能此時剛好呼叫user_set_name_id(),在name被設定為"Shang Hwang"時,user_check_name_id()開始執行,此時name等於"Shang Hwang",而id還是"J.L.",所以user_check_name_id()就會傳回FALSE,結果就顯示了錯誤訊息。
您必須同步資料對物件的更新,也就是在有一個執行緒正在設定user物件的資料時,不可以又被另一個執行緒同時進行設定,您可以使用GMutex來進行這個動作,例如:
- gmutex_demo.c
#include <glib.h>
GMutex *mutex = NULL;
struct _User {
GString *name;
GString *id;
glong count;
};
typedef struct _User User;
void user_set_name_id(User *user, GString *name, GString *id) {
if(g_mutex_trylock(mutex)) {
user->name = name;
user->id = id;
if(!user_check_name_id(user)) {
g_print("%d: illegal name or id....\n", user->count);
}
user->count++;
g_mutex_unlock(mutex);
}
else {
g_usleep(1000);
}
}
gboolean user_check_name_id(User *user) {
return user->name->str[0] == user->id->str[0];
}
gpointer thread1(gpointer user) {
GString *name = g_string_new("Justin Lin");
GString *id = g_string_new("J.L.");
while(TRUE) {
user_set_name_id(user, name, id);
}
}
gpointer thread2(gpointer user) {
GString *name = g_string_new("Shang Hwang");
GString *id = g_string_new("S.H.");
while(TRUE) {
user_set_name_id(user, name, id);
}
}
int main(int argc, char *argv[]) {
GMainLoop *mloop;
if(!g_thread_supported()) {
g_thread_init(NULL);
}
User user;
mloop = g_main_loop_new(NULL, FALSE);
mutex = g_mutex_new();
g_thread_create(thread1, &user, FALSE, NULL);
g_thread_create(thread2, &user, FALSE, NULL);
g_main_loop_run(mloop);
return 0;
}
g_mutex_trylock()會嘗試鎖定GMutex,如果成功就傳回TRUE,並繼續執行程式碼,若此時有其它的執行 緒也嘗試鑜定GMutex,則會傳回FALSE,並無法執行GMutex現已鎖定的程式碼範圍,在這個程式中,則是使用if-else,在無法鎖定時,先 睡眠1毫秒後再嘗試。