3

使用 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;
}
4

3 回答 3

3

当您从辅助线程调用 GTK 函数时,您需要将调用包装到

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

gdk_threads_enter/gdk_threads_leave通话。但是,最好只从一个线程调用 GTK 函数。最简单的方法是使用空闲函数,g_idle_add因为这将从主线程调用,但是在您的情况下,您可以将调用移至gtk_button_set_labelfrom calcfuncinto on_button1_clicked

您还应该data->idle = FALSEon_button1_clicked处理程序中设置以解决您单击按钮过快的竞争条件。

另一种方法是不使用线程,即在长时间操作期间运行 GTK 主循环。在您的循环中,您只需要泵入 Gtk 事件循环。

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

这意味着您可以避免所有线程问题和需要锁定数据访问。您可以通过检查处理程序中设置的每次迭代的布尔值来停止计算on_button1_clicked

于 2013-03-25T23:23:14.663 回答
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
于 2013-03-31T22:19:37.183 回答
0

那么在Java中我会在那个线程上调用interrupt(),然后线程会得到一个InterruptedException,并且能够在它的异常处理程序的catch中清理或者在退出之前最终阻塞。

在 C 中有几个选项:

  • 用 向该线程发送一个信号kill(),并让信号处理程序longjmp()到达您之前调用的代码中的某个点setjmp()。对于清理,您只需在setjmp()返回非零时执行某些操作,这意味着它正在从后续longjmp()调用中恢复。
  • 打电话pthread_cancel()。您在这里得到的唯一真正的清理是您之前注册的取消处理程序pthread_cleanup_push()将被调用。
  • 有一个 volatile 变量或一个锁保护变量,它会定期检查(例如,每次循环迭代一次)并在应该取消计算时设置为某个值。清理很容易,因为您可以在设置标志并跳出循环时做任何您想做的事情。

我不喜欢所有这些:信号意味着您必须处理部分故障(例如,对文件和套接字的短读取和写入)并errno==EINTR在代码中的任何地方正确处理,同时避免信号处理程序存在的各种陷阱(例如小堆栈大小以及对可以安全使用的系统调用的限制),pthread_cancel()这意味着您必须在线程函数和取消处理程序之间拖动状态,并且该标志可能会影响性能,因为它必须每次都读取未缓存的内存或锁定,如果您不要经常检查它,然后在设置标志时线程不会立即响应。

于 2013-04-03T13:40:07.960 回答