GTK+ 2.0 在Windows下的多线程应用
1 GTK+里面的线程
大家都知道,GTK+是用GLib来搭建的。那么如果你在GTK+中使用线程当然使用glib包装过的线程函数库才更合适。如果你在GTK+中用了多线程,但是又不需要和GUI进行交互,那当然就没有什么需要更多注意的了。但是,实际应用中却很难做到。我在用GTK+做第一个项目的时候还完全没有这方面的概念。现在回头想想,也许当时考虑的多一些架构会做的更好吧。
最开始用GThread的时候,想解决的是Windows上面的串口通讯问题,因为Windows串口和Linux上面的编程方法差异很大,又不想用简单的读写方法。参考了MSDN关于WIN32 API上面用多线程实现对串口的Overlape操作,直接用GTK+包裹了一个全新的GTK+ Class,有了这个实时性和功能都比较高大上了。因为我需要同时操作4个串口,而且要在串口的callback函数中根据收到的信息更新显示区域,所以开始阅读GTK+的参考手册。
手册上面提到:
GTK+ is "thread aware" but not thread safe — it provides a global lock controlled by gdk_threads_enter()/gdk_threads_leave() which protects all use of GTK+. That is, only one thread can use GTK+ at any given time
也就是说,如果我们要在线程内部去更新GUI需要用GDK里面提供的全局锁,保证每个线程要完成的GUI任务安全结束,要知道如果在刷新GUI的过程中被中断就会造成GUI的显示甚至是运行异常。在GTK+的参考手册中,提供了一个简单的例子。还是贴出来一下哦。
这个gtk-thread例子采用的是之前标准GTK+使用多线程更新UI的方法,我们如果偷懒一点直接在www.google.com去搜索GTK+多线程搜索到的方法也就是这个。这里稍加说明:
- g_thread_init目的是要让这个GObject的动态系统支持多线程,在GTK+2.24.10以后的版本中默认就已经支持多线程系统,不再需要调用这个函数了。
- gdk_threads_init 这个函数是用来初始化GTK+在多线程时使用的全局锁,所以必须放在gtk_init之前。
- gtk_main必须被gdk_threads_enter和gdk_threads_leave包裹,那么何时调用gdk_threads_enter取决与你的线程何时启动何时需要UI同步,举例说明一下,如果你启动了一个线程很早就需要同步对GUI进行刷新,那么你就要在你调用线程的刷新之前调用它。
2 GTK+多线程在Windows上的应用方法
继续阅读GTK+的手册,大家会发现如果是在Microsoft Windows上用GTK+,并没有想像的那么乐观。下面这句话是手册中提到的:
Unfortunately the above holds with the X11 backend only. With the Win32 backend, GDK calls should not be attempted from multiple threads at all.
尽管如此在GTK+2.24.10之前的版本里,在Windows的环境下gdk_threads_enter这样的方法是可以应用的。本人得益与此,却也深受其害。参考gtk-thread这样的例子,让我的项目得以顺利进行,但也因为GTK+后续的升级问题着实让我绞尽脑汁。多种原因使得我必须升级我应用的GTK+的版本。但是之前Windows上基于以上 gdk_threads_enter 这样同步机制的所有程序只要稍微拖动一下窗口程序就会进入死锁状态,gtk-thread也一样。因为在Windows上用GTK+人不多,所以去google上搜索很难有正确的答案。
索性破釜沉舟,升级了最新的GTK+的开发环境,最新的GDK的手册是这样说的:
GTK+, however, is not thread safe. You should only use GTK+ and GDK from the thread gtk_init() and gtk_main() were called on. This is usually referred to as the “main thread”. Signals on GTK+ and GDK types, as well as non-signal callbacks, are emitted in the main thread. You can schedule work in the main thread safely from other threads by using gdk_threads_add_idle() and gdk_threads_add_timeout()
其实,在之前的项目中为了能在自己的thread中去创建对话框,gdk_threads_add_idle这样的函数用过,个人认为这个不应该是死锁产生的原因。但是经过测试,如果只用gdk_threads_add_idle 这样的方法的确UI不会死锁。但是,如果之前应用gdk_threads_enter 这样的代码全部改成 gdk_threads_add_idle 将是一个巨大而且痛苦的工作。原因有两个:
- 之前对于GTK+ UI的刷新部份,都是线程代码的一部分,如果用 gdk_threads_add_idle 就一定要把这些代码放到一个新的callback函数中去做。
- 即便是我重新写好了无数个callback,那么参数传入将又是一个巨大工作,因为callback函数原则上只有一个传入指针,而我之前对于UI刷新的部份代码写的很随意,基本上没有统一的结构体能够函盖,要知道我用GTK+都是用C来写的啊。
思考这些问题的同时,我们也把网上比较好的一个idle的例子贴近来:在gtk-idle的例子中,线程会把UI的处理部份让GTK+的主线程来处理,如果大家经常多线程应该很快就有疑惑了,对于线程本身来说,怎么确定自己要对UI做对事情已经做好能。就好比之前我们应用的 gdk_threads_leave 函数呢?
3 GCC内嵌函数给GObject一个闭包的机会
当我遇到上一个章节的 gdk_threads_add_idle 需要克服的困难是,让我想到了之前Java中处理event采用的闭包的方法,比如java-event这个样子。关于闭包的概念,大家可以去看参考wikihttps://en.wikipedia.org/wiki/Closure。
我们从java-event例子中的代码可以看到,虽然我们定义的按键事件只是传入了一个参数,但是因为闭包的特性,实际上在事件处理的内部我们几乎可以引用initialize方法中的所有内部变量。另外,这样的event函数在 initialize 方法内部就可以定义。如果我们在GTK+中能够实现这样的应用。那么,在处理多线程对UI的操作是基本上就是每个线程相对独立的。这样的应用和之前的 gdk_threads_enter 这样的方法用起来很像。
重新在回到我们的GTK+中,我用的是GTK+最原始的开发环境,开发语言就是C。如果从开发的语言来看,C语言本身是不支持闭包的。但很幸运的一点是,我用GCC来编译我的代码。而GCC恰恰为C语言提供了个扩展功能内部函数 。
double foo(double a, double b){ double square(double z){ return z*z; } printf("Function: %s ",__func__); return square(a)+square(b); }
从某种意义上来说,如果外部程序可以调用函数内部的内部函数,对于我的应用来说基本达到了闭包的需求。而另一个需要解决的问题就是,同步这个内部函数的执行过程。这个问题解决比较容易只要通过GMutex GCond就可以完成。为了完成这样的工作,我定义了一个新的GObject Class:
- GtkThreadUpdater
- gtk_thread_updater_new 用来创建用来更新UI的对象
- gtk_thread_updater_return 需要放到我们定义事件处理函数的返回之前,用来和调用线程同步。
- gtk_thread_updater_run 对GTK+的 gdk_threads_add_idle 函数进行了封装,当事件处理函数结束的时候才会返回。
应用定义好的class重写之前的gtk-idle例子,参考gtk-updater代码。
从这个代码中我们可以看到:我们可以在我们自己线程中,直接定义个更新GTK GUI的函数,这个函数有一个传入指针。同时可以调用线程内部的变量
GtkThreadUpdater *thread_updater = gtk_thread_updater_new(); gboolean update(GtkEntry *entry) { gtk_entry_progress_pulse (entry); gtk_thread_updater_return(thread_updater); return FALSE; } gtk_thread_updater_run(thread_updater,(GSourceFunc)update, entry); g_object_unref(thread_updater);
相当于我们在2.24.10之前下面的写法:
gdk_threads_enter (); gtk_entry_progress_pulse (entry); gdk_threads_leave ();
4 结论
至此,我们如果使用了GTK+2.24.10以后的版本,并且频繁的使用多线程,我们有了一套更方便的API完成GUI的更新。有了这个,我很容易的把2.24.10之前的代码更新到新的GTK+.当然,前提是你要使用GCC编译器。推荐大家用msys2里面的mingw64。
#include <gtk/gtk.h> #include <unistd.h> #include "gtkthreadupdater.h" static gboolean terminate (GThread *thread) { g_thread_join (thread); return FALSE; } static void * thread_func (GtkEntry *entry) { int i; for (i = 0; i < 100; i++) { usleep (10000); /* 0.1 s */ GtkThreadUpdater *thread_updater = gtk_thread_updater_new(); gboolean update(GtkEntry *entry) { gtk_entry_progress_pulse (entry); gtk_thread_updater_return(thread_updater); return FALSE; } gtk_thread_updater_run(thread_updater,(GSourceFunc)update, entry); g_object_unref(thread_updater); } /* Make sure this thread is joined properly */ gdk_threads_add_idle ((GSourceFunc)terminate, g_thread_self ()); return NULL; } int main (int argc, char **argv) { GtkWidget *window, *entry; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_default_size (GTK_WINDOW (window), 200, -1); g_signal_connect (window, "destroy", gtk_main_quit, NULL); entry = gtk_entry_new (); gtk_container_add (GTK_CONTAINER (window), entry); gtk_widget_show_all (window); /* Start thread */ g_thread_new ("dummy", (GThreadFunc)thread_func, entry); gtk_main (); return 0; }
#include "gtkthreadupdater.h" struct GtkThreadUpdaterPrivate_ { /* add your private declarations here */ GMutex *mutex; GCond *cond; }; static void gtk_thread_updater_finalize (GObject *object); G_DEFINE_TYPE_WITH_PRIVATE(GtkThreadUpdater, gtk_thread_updater, G_TYPE_OBJECT) static void gtk_thread_updater_class_init (GtkThreadUpdaterClass *klass) { GObjectClass *g_object_class; g_object_class = G_OBJECT_CLASS (klass); g_object_class->finalize = gtk_thread_updater_finalize; } static void gtk_thread_updater_finalize (GObject *object) { GtkThreadUpdater *self; g_return_if_fail (GTK_IS_THREAD_UPDATER (object)); self = GTK_THREAD_UPDATER (object); g_free(self->priv->cond); g_free(self->priv->mutex); G_OBJECT_CLASS (gtk_thread_updater_parent_class)->finalize (object); } static void gtk_thread_updater_init (GtkThreadUpdater *self) { self->priv = gtk_thread_updater_get_instance_private(self); self->priv->mutex = g_new(GMutex,1); self->priv->cond = g_new(GCond,1); g_mutex_init(self->priv->mutex); g_cond_init(self->priv->cond); } void gtk_thread_updater_return (GtkThreadUpdater *self) { g_mutex_lock(self->priv->mutex); g_cond_signal(self->priv->cond); g_mutex_unlock(self->priv->mutex); } void gtk_thread_updater_run (GtkThreadUpdater *self, GSourceFunc function, gpointer pointer) { g_mutex_lock(self->priv->mutex); gdk_threads_add_idle ((GSourceFunc)function, pointer); g_cond_wait(self->priv->cond,self->priv->mutex); g_mutex_unlock(self->priv->mutex); } GtkThreadUpdater * gtk_thread_updater_new (void) { return g_object_new (GTK_TYPE_THREAD_UPDATER, NULL); }
#ifndef GTKTHREADUPDATER_H_ #define GTKTHREADUPDATER_H_ 1 #include <glib-object.h> #include <gtk/gtk.h> G_BEGIN_DECLS #define GTK_TYPE_THREAD_UPDATER (gtk_thread_updater_get_type ()) #define GTK_THREAD_UPDATER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_THREAD_UPDATER, GtkThreadUpdater)) #define GTK_THREAD_UPDATER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_THREAD_UPDATER, GtkThreadUpdaterClass)) #define GTK_IS_THREAD_UPDATER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_THREAD_UPDATER)) #define GTK_IS_THREAD_UPDATER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_THREAD_UPDATER)) #define GTK_THREAD_UPDATER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_THREAD_UPDATER, GtkThreadUpdaterClass)) typedef struct GtkThreadUpdater_ GtkThreadUpdater; typedef struct GtkThreadUpdaterClass_ GtkThreadUpdaterClass; typedef struct GtkThreadUpdaterPrivate_ GtkThreadUpdaterPrivate; struct GtkThreadUpdater_ { GObject parent; /* add your public declarations here */ GtkThreadUpdaterPrivate *priv; }; struct GtkThreadUpdaterClass_ { GObjectClass parent_class; }; GType gtk_thread_updater_get_type (void); GtkThreadUpdater *gtk_thread_updater_new (void); void gtk_thread_updater_return (GtkThreadUpdater *self); void gtk_thread_updater_run (GtkThreadUpdater *self, GSourceFunc function, gpointer pointer); G_END_DECLS #endif /* GTKTHREADUPDATER_H_ */
package com.rickleaf;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.JButton;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class Btnevent extends JFrame {
private JPanel contentPane;
/**
* Launch the application.
*/
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
Btnevent frame = new Btnevent();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
/**
* Create the frame.
*/
public Btnevent() {
initialize();
}
private void initialize() {
final String test = "Java Event Test.";
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 100, 450, 300);
contentPane = new JPanel();
contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
setContentPane(contentPane);
contentPane.setLayout(null);
JButton btnNewButton = new JButton("New button");
btnNewButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
JOptionPane.showMessageDialog(null, test);
}
});
btnNewButton.setBounds(188, 101, 89, 23);
contentPane.add(btnNewButton);
}
}
/*------------------------------------------------------------------------- * Filename: gtk-thread.c * Version: 0.99.1 * Copyright: Copyright (C) 1999, Erik Mouw * Author: Erik Mouw <J.A.K.Mouw@its.tudelft.nl> * Description: GTK threads example. * Created at: Sun Oct 17 21:27:09 1999 * Modified by: Erik Mouw <J.A.K.Mouw@its.tudelft.nl> * Modified at: Sun Oct 24 17:21:41 1999 *-----------------------------------------------------------------------*/ /* * Compile with: * * cc -o gtk-thread gtk-thread.c `gtk-config --cflags --libs gthread` * * Thanks to Sebastian Wilhelmi and Owen Taylor for pointing out some * bugs. * */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> #include <gtk/gtk.h> #include <glib.h> #include <pthread.h> #define YES_IT_IS (1) #define NO_IT_IS_NOT (0) typedef struct { GtkWidget *label; int what; } yes_or_no_args; G_LOCK_DEFINE_STATIC (yes_or_no); static volatile int yes_or_no = YES_IT_IS; void destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } void *argument_thread (void *args) { yes_or_no_args *data = (yes_or_no_args *)args; gboolean say_something; for (;;) { /* sleep a while */ sleep(rand() / (RAND_MAX / 3) + 1); /* lock the yes_or_no_variable */ G_LOCK(yes_or_no); /* do we have to say something? */ say_something = (yes_or_no != data->what); if(say_something) { /* set the variable */ yes_or_no = data->what; } /* Unlock the yes_or_no variable */ G_UNLOCK (yes_or_no); if (say_something) { /* get GTK thread lock */ gdk_threads_enter (); /* set label text */ if(data->what == YES_IT_IS) gtk_label_set_text (GTK_LABEL (data->label), "O yes, it is!"); else gtk_label_set_text (GTK_LABEL (data->label), "O no, it isn't!"); /* release GTK thread lock */ gdk_threads_leave (); } } return NULL; } int main (int argc, char *argv[]) { GtkWidget *window; GtkWidget *label; yes_or_no_args yes_args, no_args; pthread_t no_tid, yes_tid; /* init threads */ g_thread_init (NULL); gdk_threads_init (); gdk_threads_enter (); /* init gtk */ gtk_init(&argc, &argv); /* init random number generator */ srand ((unsigned int) time (NULL)); /* create a window */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); g_signal_connect (window, "destroy", G_CALLBACK (destroy), NULL); gtk_container_set_border_width (GTK_CONTAINER (window), 10); /* create a label */ label = gtk_label_new ("And now for something completely different ..."); gtk_container_add (GTK_CONTAINER (window), label); /* show everything */ gtk_widget_show (label); gtk_widget_show (window); /* create the threads */ yes_args.label = label; yes_args.what = YES_IT_IS; pthread_create (&yes_tid, NULL, argument_thread, &yes_args); no_args.label = label; no_args.what = NO_IT_IS_NOT; pthread_create (&no_tid, NULL, argument_thread, &no_args); /* enter the GTK main loop */ gtk_main (); gdk_threads_leave (); return 0; }
#include <gtk/gtk.h> static gboolean update_entry (GtkEntry *entry) { gtk_entry_progress_pulse (entry); return FALSE; } static gboolean terminate (GThread *thread) { g_thread_join (thread); return FALSE; } static void * thread_func (GtkEntry *entry) { int i; for (i = 0; i < 100; i++) { usleep (100000); /* 0.1 s */ gdk_threads_add_idle ((GSourceFunc)update_entry, entry); } /* Make sure this thread is joined properly */ gdk_threads_add_idle ((GSourceFunc)terminate, g_thread_self ()); return NULL; } int main (int argc, char **argv) { GtkWidget *window, *entry; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_default_size (GTK_WINDOW (window), 200, -1); g_signal_connect (window, "destroy", gtk_main_quit, NULL); entry = gtk_entry_new (); gtk_container_add (GTK_CONTAINER (window), entry); gtk_widget_show_all (window); /* Start thread */ g_thread_new ("dummy", (GThreadFunc)thread_func, entry); gtk_main (); return 0; }