2013-03-25 114 views
2

使用GTK和C,如何使用按鈕啓動/停止長計算(在單獨的線程中)?我的工作代碼就是這樣做的,但我對這是一個合理的方法(即「正確的」)沒有多少信心。使用GTK和C,如何使用按鈕啓動/停止長計算(在單獨的線程中)?

我有一個按鈕,其標籤切換從「開始」「停止」。我也有一個全局pthread_t變量來存儲一個線程。我的方法是通過按鈕的單擊信號處理程序啓動或取消線程,具體取決於全局布爾類型的「空閒」標誌的值,該標誌指示線程當前是否正在運行。

我想工作精心設計的最小測試用例,這樣我可以很容易地理解代碼爲更大的計劃相適應。這個問題與Python&PyGTK: Stop while on button click非常相似,但是這個問題在Python中我不知道。

我的代碼---貼在下面 - 似乎工作,但我不相信它,因爲我可以很容易地只需點擊開始使系統癱瘓/停止按鈕幾次快速連續。

我很好奇,看看別人怎麼會(獨立)解決這個問題,怎麼他們的做法比較雷,也是一個代碼審查我自己的方法,如果它實際上是一個體面的方式。

#include <gtk/gtk.h> 
#include <pthread.h> 

/* suppress unused variable warnings */ 
#define UNUSED(x) (void)(x) 

typedef struct _Data { 
    GtkWidget *window1, 
       *button1; 
    gint idle; 
    pthread_t calcthread; 
} Data; 

static Data *data; 

void *calcfunc(void *arg) { 
    int i; 
    UNUSED(arg); 

    data->idle=FALSE; 
    gtk_button_set_label(GTK_BUTTON(data->button1),"Stop"); 

    /* This is intended to simulated a long calculation that may finish. 
     Adjust the limit as needed */ 
    for(i=1;i<2e9;++i) { 
    } 

    data->idle=TRUE; 
    pthread_exit(NULL); 
} 

/* this is our click event handler.... it suppose to start or stop 
    the "calcthread" depending on the value of the "idle" flag */ 
void on_button1_clicked(GtkWidget *widget, Data *ldata) { 
    int ret; 
    UNUSED(widget); 
    UNUSED(ldata); 

    if (data->idle==TRUE) { 
     printf("idle.. starting thread\n"); 
     ret=pthread_create(&data->calcthread, NULL, calcfunc, NULL); 
     if (ret !=0) { 
      g_error("ERROR: could not create thread\n"); 
     } 
    } else { 
     printf("not idle... canceling thread..."); 
     ret= pthread_cancel(data->calcthread); 
     if (ret != 0) { 
      g_error("ERROR: could not cancel thread\n"); 
     } else { 
      printf("canceled\n"); 
     } 
     data->idle=TRUE; 
     gtk_button_set_label(GTK_BUTTON(data->button1),"start"); 
    } 
} 

/* just defines our setup */ 
int main (int argc, char *argv[]) { 

    g_thread_init(NULL); 
    gdk_threads_init(); 
    gdk_threads_enter(); 

    gtk_init(&argc, &argv); 

    data=g_slice_new0(Data); 
    data->idle=TRUE; /* initial state */ 

    printf("idle is %d\n",data->idle); 

    /* add widgets and objects to our structure */ 

    data->window1=gtk_window_new(GTK_WINDOW_TOPLEVEL); 
    gtk_window_set_default_size(GTK_WINDOW(data->window1),250,250); 
    data->button1=gtk_button_new_with_label("Start"); 
    gtk_container_add(GTK_CONTAINER(data->window1),GTK_WIDGET(data->button1)); 

    gtk_signal_connect(GTK_OBJECT(data->window1), "delete-event", 
         gtk_main_quit, NULL); 
    gtk_signal_connect(GTK_OBJECT(data->button1), "clicked", 
          G_CALLBACK(on_button1_clicked), NULL); 

    gtk_widget_show_all(GTK_WIDGET(data->window1)); 

    gtk_main(); 

    /* Don't forget to free the memory! */ 
    g_slice_free(Data, data); 

    gdk_threads_leave(); 

    return 0; 
} 
+0

這裏有一些競爭條件,例如,你的回調檢查空閒標誌併產生線程,但是在那時,回調可以再次運行並且在calcfunc有機會運行之前檢查空閒(調度器被允許按照線程執行的順序做任何事情),然後你已經產生了兩個線程,但不再有第一個線程的句柄。控制器線程跟蹤空閒/運行可能會更好。 – engineerC 2013-03-25 23:14:54

回答

3

當你從你需要調用換到

gtk_button_set_label(GTK_BUTTON(data->button1),"Stop"); 

gdk_threads_enter/gdk_threads_leave調用輔助線程調用GTK的功能。但是,最好只從一個線程調用GTK函數。最簡單的方法是使用g_idle_add,因爲這將在主線程被稱爲空閒功能,但在你的情況,你可以只從calcfunc移動呼叫gtk_button_set_labelon_button1_clicked

您還應該在on_button1_clicked處理程序中設置data->idle = FALSE以解決單擊按鈕過快的競爭條件。

你可以做到這一點的另一種方式是無緒,那就是長期運行過程中運行GTK主循環。在你的循環中,你只需要抽取Gtk事件循環。

for(i=1;i<2e9;++i) { 
    while (gtk_events_pending()) { 
     gtk_main_iteration(); 
    } 
} 

這意味着您可以避免所有線程問題並需要鎖定數據訪問。您可以通過檢查在on_button1_clicked處理程序中設置的每個迭代的布爾值來停止計算。

+0

gtk_events_pending循環是我以前處理事情的方式。由於每次迭代都會調用該函數,因此它看起來非常昂貴。我目前正在修改我的示例以利用您的建議,但我仍然在努力實現其目標。 – 2013-03-26 01:20:38

+0

根據我對GDK線程文檔的瞭解,我的gtk_button_set_label()調用仍在主GDK鎖中,因此不需要gdk_threads_enter/gdk_threads_leave保護。它僅在gdid_add之類的函數中需要,它們位於主GDK鎖之外。 – 2013-03-26 03:52:22

+0

所有的呼叫都需要在GDK Lock內部進行串行化訪問。查看第二個示例中的'argument_thread'函數:https://developer.gnome.org/gtk-faq/stable/x481.html – iain 2013-03-26 12:26:54

1

以下代碼完成我所要求的操作。它使用pthreads。我不知道這是否是最優雅的,但它似乎工作。訣竅是使用兩個標誌:一個用於空閒狀態,另一個用於取消請求,這樣就避免了需要使用「pthread_cancel」函數取消該線程,我發現這在實際代碼中是不尋常的。

#include <gtk/gtk.h> 
#include <pthread.h> 
#include <errno.h> 
#include <stdlib.h> 

#define UNUSED(x) (void)(x) 

#define handle_error_en(en, msg) do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0) 

typedef struct _Data { 
    GtkWidget *window1, 
       *button1; 
} Data; 

static Data *data; 

static pthread_mutex_t calcmutex = PTHREAD_MUTEX_INITIALIZER; 
static pthread_t calcthread=0; 

static gboolean idle=TRUE,cancel_request=FALSE; 

void *calcfunc(void *arg) { 
    int i,s; 
    UNUSED(arg); 
    g_print("\tstarting thread\n"); 

    s = pthread_setcancelstate(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); 
    if (s != 0) { 
     handle_error_en(s, "pthread_setcancelstate"); 
    } 

    gdk_threads_enter(); 
    gtk_button_set_label(GTK_BUTTON(data->button1),"Stop"); 
    gdk_threads_leave(); 

    g_print("\tstarting work...\n"); 
    for (i=0; i<100000000 ;++i) { 

     /* check for cancelation */ 
     pthread_mutex_lock(&calcmutex); 
     if (cancel_request) { 
      g_print("\t[cancel request noted].\n"); 
      pthread_mutex_unlock(&calcmutex); 
      break; 
     } 
     pthread_mutex_unlock(&calcmutex); 

     /* do "calculation" */ 
     i=i*1*-1*1*-1; 
    } 
    g_print("\tdone work.\n"); 

    gdk_threads_enter(); 
    gtk_button_set_label(GTK_BUTTON(data->button1),"Start"); 
    gdk_threads_leave(); 

    pthread_mutex_lock(&calcmutex); 
    cancel_request=FALSE; 
    idle=TRUE; 
    pthread_mutex_unlock(&calcmutex); 

    g_print("\tdone thread.\n"); 
    pthread_exit(NULL); 
} 

void on_button1_clicked(GtkWidget *widget, gpointer *ldata) { 
    int s; 
    UNUSED(widget); 
    UNUSED(ldata); 
    g_print("entered on_button1_clicked\n"); 


    pthread_mutex_lock(&calcmutex); 
    if (idle) { 
     g_print("idle, starting thread\n"); 
     s = pthread_create(&calcthread, NULL, calcfunc, NULL); 
     if (s != 0) { 
      handle_error_en(s, "pthread_create"); 
     } 
     idle=FALSE; 
    } else { 
     g_print("not idle and not first time, making canceling request.\n"); 
     cancel_request=TRUE; 
    } 
    pthread_mutex_unlock(&calcmutex); 

    g_print("finished on_button1_clicked\n"); 
} 

/* just defines our setup */ 
int main (int argc, char *argv[]) { 

    g_thread_init(NULL); 
    gdk_threads_init(); 
    gdk_threads_enter(); 

    gtk_init(&argc, &argv); 

    data=g_slice_new0(Data); 

    printf("initial idle is %d\n",idle); 

    /* add widgets and objects to our structure */ 
    data->window1=gtk_window_new(GTK_WINDOW_TOPLEVEL); 
    gtk_window_set_default_size(GTK_WINDOW(data->window1),250,250); 
    data->button1=gtk_button_new_with_label("Start"); 
    gtk_container_add(GTK_CONTAINER(data->window1),GTK_WIDGET(data->button1)); 

    gtk_signal_connect(GTK_OBJECT(data->window1), "delete-event", 
         gtk_main_quit, NULL); 
    gtk_signal_connect(GTK_OBJECT(data->button1), "clicked", 
          G_CALLBACK(on_button1_clicked), NULL); 

    gtk_widget_show_all(GTK_WIDGET(data->window1)); 

    gtk_main(); 

    /* free the memory and stuff */ 
    g_slice_free(Data, data); 
    pthread_mutex_destroy(&calcmutex); 

    gdk_threads_leave(); 

    return 0; 
} 

這將編譯與

gcc -Wall -Wextra -Wconversion -pedantic `pkg-config --cflags --libs gtk+-2.0` start_stop.c -o start_stop 
0

在Java中我會調用該線程中斷(),則該線程將然後得到一個InterruptedException warningless,並能夠在其異常處理程序的清理在退出之前趕上或最後阻止。

在C有幾個選項:

  • 發送線程的信號與kill(),並具有信號處理longjmp()在代碼中的一個點,你以前被稱爲setjmp()。對於清理,當setjmp()返回非零時,您只需執行一些操作,這意味着它將從後續的longjmp()調用中恢復。
  • 致電pthread_cancel()。您在這裏得到的唯一真正的清理是您之前在pthread_cleanup_push()註冊的取消處理程序將被調用。
  • 有一個volatile變量或一個lock受保護的變量,它會定期檢查(例如每次循環迭代一次)並在計算被取消時設置爲某個值。清理工作非常簡單,因爲您可以在標誌設置完成後執行任何您喜歡的操作,並且您可以跳出循環。

我不喜歡所有的人(如:短讀取和對文件和套接字寫入)信號意味着你必須處理局部故障和errno==EINTR在你的代碼正確無處不在,同時避免與信號處理程序存在的各種陷阱的(比如小堆棧大小和可以安全使用哪些系統調用的限制),pthread_cancel()意味着您必須在線程函數和取消處理程序之間拖動狀態,並且該標誌可能會影響性能,因爲它必須讀取未緩存的內存或每次都要鎖定一個鎖,如果你沒有經常檢查它,那麼線程在設置標誌時不會立即響應。

相關問題