Glib之主事件循环

介绍

GLib和GTK+应用的主事件循环管理着所有事件源。这些事件的来源有很多种比如文件描述符(文件、管道或套接字)或超时。新类型的事件源可以通过g_source_attach()函数添加。
为了让多组独立事件源能够在不同的线程中被处理,每个事件源都会关联一个GMainContext。一个线程只能运行一个GMainContext,但是在其他线程中能够对事件源进行添加和删除操作。
每个事件源都被赋予了优先级。默认的优先级是G_PRIORITY_DEFAULT(0)。值越小优先级越高,优先级高的事件源优先处理。
Idle函数在没有更高优先级的事件被处理的时候才会执行。
GMainLoop数据类型代表了一个主事件循环。通过g_main_loop_new()来创建GMainLoop对象。在添加完初始事件源后执行g_main_loop_run(),主循环将持续不断的检查每个事件源产生的新事件,然后分发它们,直到处理来自某个事件源的事件的时候触发了g_main_loop_quit()调用退出主循环为止。
GMainLoop实例能够被递归创建。在GTK+应用中经常使用这种方式来显示模态对话框。注意如果一个事件源被添加到一个GMainContext,那么它将被所有关联这个GMainContext的主线程检查和分发。
GTK+对这些函数做了些封装,例如gtk_main、gtk_mian_quit和gtk_events_pending。

自定义事件类型

GMainLoop一个不常用的特性就是能够创建一个新的事件源类型,然后当做内置事件源的扩展来使用。一个新的事件源类型通常用来处理GDK事件。通过继承GSource结构来创建一个新的事件源类型。继承产生的新事件源类型表示GSource结构作为新事件源类型的第一个元素然后其他元素紧跟其后。使用g_source_new函数来创建新的事件源类型实例,函数的参数就是新的事件源类型大小。GSourceFuncs决定新的事件源类型的行为。
新的事件源有两种基本方式与GMainContext交互。它们GSourceFuncs中的准备函数能够设置睡眠事件,用来决定主循环下次检测它们的时间。此外事件源也可以使用g_source_add_poll()函数添加文件描述符到GMainContext进行检测。

自定义主循环迭代

执行g_main_context_iteration()函数可以完成GMainContext的单次迭代。在一些情况下,我们可能想获取主循环更多的底层控制细节,我们可以调用g_main_context_iteration()里的组件函数:g_main_context_prepare()g_main_context_prepare ()g_main_context_query()g_main_context_check()g_main_context_dispatch()

Main Context状态

在UNIX系统上,GLib的主循环和fork()是不兼容的。

事件源内存管理

有两种可选的方式来管理传递给GSource回调函数用户数据的内存。用户数据就是在调用g_timeout_add()g_timeout_add_full()g_idle_add()传入的参数。这些数据通常被timeout或idle回调函数所拥有,比如一个构件或一个网路协议的实现。有些时候这些回调函数会在数据被销毁的后背调用,因为使用了已经被释放的内存,所以这会导致一个错误。

  • 第一种推荐的方法就是保存g_timeout_add()g_source_attach()返回的事件源ID,然后在其维持的用户数据被释放后显示的将其从GMainContext移除。这样就能保证调用这些回调函数的时候,这些用户数据依然有效。
  • 第二种就是保存这些回调函数中的用户数据对象的引用,然后在GDestroyNotify回调函数中释放它。这样就能确保数据对象在事件源最后一次调用然后被释放前一直有效。GDestroyNotify回调函数是GSource函数的一个变体的入参,它在事件源被释放时调用。
    第二条途径有必要提醒下,如果在事件源还没被调用前主循环就结束的情况下,用户数据对象被维持状态是不确定的。

代码

数据结构

struct _GSourcePrivate
{
  GSList *child_sources;
  GSource *parent_source;

  gint64 ready_time;

  /* This is currently only used on UNIX, but we always declare it (and
   * let it remain empty on Windows) to avoid #ifdef all over the place.
   */
  GSList *fds;
};

struct _GSource
{
  /*< private >*/
  gpointer callback_data;
  GSourceCallbackFuncs *callback_funcs;

  const GSourceFuncs *source_funcs;
  guint ref_count;

  GMainContext *context;

  gint priority;
  guint flags;
  guint source_id;

  GSList *poll_fds;

  GSource *prev;
  GSource *next;

  char    *name;

  GSourcePrivate *priv;
};

struct _GPollRec
{
  GPollFD *fd;
  GPollRec *prev;
  GPollRec *next;
  gint priority;
};

struct _GMainContext
{
  /* The following lock is used for both the list of sources
   * and the list of poll records
   */
  GMutex mutex;
  GCond cond;
  GThread *owner;
  guint owner_count;
  GSList *waiters;

  gint ref_count;

  GHashTable *sources;              /* guint -> GSource */

  GPtrArray *pending_dispatches;
  gint timeout;            /* Timeout for current iteration */

  guint next_id;
  GList *source_lists;
  gint in_check_or_prepare;

  GPollRec *poll_records;
  guint n_poll_records;
  GPollFD *cached_poll_array;
  guint cached_poll_array_size;

  GWakeup *wakeup;

  GPollFD wake_up_rec;

/* Flag indicating whether the set of fd's changed during a poll */
  gboolean poll_changed;

  GPollFunc poll_func;

  gint64   time;
  gboolean time_is_fresh;
};

struct _GMainLoop
{
  GMainContext *context;
  gboolean is_running;
  gint ref_count;
};
函数 说明
g_main_context_add_poll_unlocked 在g_source_add_poll和g_source_add_unix_fd中被调用,首先将需要监听的文件描述符保存到_GSource->poll_fds或_GSource->priv->fds中,然后再创建个_GPollRec对象,接着将其保存到_GMainContext->poll_records中,最后设置context->poll_changed为True,这个值会影响当前主循环的g_main_context_check
g_source_attach_unlocked 在g_source_attach和g_source_add_child_source中被调用,将_GSource保存到_GMainContext->sources,并将_GSource保存的文件描述符通过g_main_context_add_poll_unlocked函数保存到_GMainContext,最后返回_GSource->source_id
g_main_context_prepare 遍历_GMainContext拥有的事件源,调用事件源的_GSourceFuncs->prepare函数计算下次轮训间隔,并检测事件源是否已经就绪
g_main_context_query 从_GMainContext拥有的事件源中筛选出需要进行poll的事件源,并设置context->poll_changed为False(只要不是在poll期间对事件源进行添加或删除操作即可)
g_main_context_poll 使用poll函数监听g_main_context_query筛选出的事件源
g_main_context_check 遍历g_main_context_query筛选出的事件源,调用事件源的_GSourceFuncs->check函数检测事件源是否已经就绪,如果就绪则将事件源添加到_GMainContext->pending_dispatches中。如果context->poll_changed为True(说明在poll的时候有新的事件源加入或移除),则跳过后面的分发(pending_dispatches为空),重新执行g_main_context_iterate
g_main_context_dispatch 遍历_GMainContext->pending_dispatches中的事件源,并调用事件源的_GSourceFuncs->dispatch函数进行分发

idle事件源

#define G_PRIORITY_DEFAULT_IDLE     200

GSourceFuncs g_idle_funcs =
{
  g_idle_prepare,
  g_idle_check,
  g_idle_dispatch,
  NULL
};

/* Idle functions */

static gboolean g_idle_prepare(GSource *source, gint *timeout)
{
  *timeout = 0;

  return TRUE;
}

static gboolean g_idle_check(GSource *source)
{
  return TRUE;
}

static gboolean g_idle_dispatch (GSource *source, GSourceFunc callback, gpointer user_data)
{
  gboolean again;

  if (!callback)
    {
      g_warning ("Idle source dispatched without callback\n"
         "You must call g_source_set_callback().");
      return FALSE;
    }

  again = callback (user_data);

  TRACE (GLIB_IDLE_DISPATCH (source, source->context, callback, user_data, again));

  return again;
}
函数 说明
g_idle_add 创建一个idle事件源,然后添加到GMainContext

timeout事件源

#define G_PRIORITY_DEFAULT          0

GSourceFuncs g_timeout_funcs =
{
  NULL, /* prepare */
  NULL, /* check */
  g_timeout_dispatch,
  NULL
};

static gboolean g_timeout_dispatch (GSource *source, GSourceFunc callback, gpointer user_data)
{
  GTimeoutSource *timeout_source = (GTimeoutSource *)source;
  gboolean again;

  if (!callback)
    {
      g_warning ("Timeout source dispatched without callback\n"
                 "You must call g_source_set_callback().");
      return FALSE;
    }

  again = callback (user_data);

  TRACE (GLIB_TIMEOUT_DISPATCH (source, source->context, callback, user_data, again));

  if (again)
    g_timeout_set_expiration (timeout_source, g_source_get_time (source));

  return again;
}
函数 说明
g_timeout_add 创建一个timeout事件源,然后添加到GMainContext。timeout事件源没有自己的prepare和check函数,是因为在g_main_context_prepare和g_main_context_check 中都会获取下当前系统时间,然后和timeout事件源的对比,如果时间已经过了,则将事件源的状态修改为就绪状态,所以就不需要自己来写了

GIOChannel事件源

struct _GIOChannel
{
  /*< private >*/
  gint ref_count;
  GIOFuncs *funcs;

  gchar *encoding;
  GIConv read_cd;
  GIConv write_cd;
  gchar *line_term; /* String which indicates the end of a line of text */
  guint line_term_len; /* So we can have null in the line term */

  gsize buf_size;
  GString *read_buf; /* Raw data from the channel */
  GString *encoded_read_buf;    /* Channel data converted to UTF-8 */
  GString *write_buf; /* Data ready to be written to the file */
  gchar partial_write_buf[6];    /* UTF-8 partial characters, null terminated */

  /* Group the flags together, immediately after partial_write_buf, to save memory */

  guint use_buffer     : 1;    /* The encoding uses the buffers */
  guint do_encode      : 1;    /* The encoding uses the GIConv coverters */
  guint close_on_unref : 1;    /* Close the channel on final unref */
  guint is_readable    : 1;    /* Cached GIOFlag */
  guint is_writeable   : 1;    /* ditto */
  guint is_seekable    : 1;    /* ditto */

  gpointer reserved1;
  gpointer reserved2;
};

struct _GIOFuncs
{
  GIOStatus (*io_read)(GIOChannel *channel, gchar *buf, gsize count, gsize *bytes_read, GError **err);
  GIOStatus (*io_write)(GIOChannel *channel, const gchar *buf, gsize count, gsize *bytes_written, GError **err);
  GIOStatus (*io_seek)(GIOChannel *channel, gint64 offset, GSeekType type, GError **err);
  GIOStatus (*io_close)(GIOChannel *channel, GError **err);
  GSource* (*io_create_watch)(GIOChannel *channel, GIOCondition  condition);
  void (*io_free)(GIOChannel *channel);
  GIOStatus (*io_set_flags)(GIOChannel *channel, GIOFlags flags, GError **err);
  GIOFlags (*io_get_flags)(GIOChannel *channel);
};

GSourceFuncs g_io_watch_funcs = {
  g_io_unix_prepare,
  g_io_unix_check,
  g_io_unix_dispatch,
  g_io_unix_finalize
};

static GIOFuncs unix_channel_funcs = {
  g_io_unix_read,
  g_io_unix_write,
  g_io_unix_seek,
  g_io_unix_close,
  g_io_unix_create_watch,
  g_io_unix_free,
  g_io_unix_set_flags,
  g_io_unix_get_flags,
};
函数 说明
g_io_channel_new_file 获取并保存文件句柄,然后使用unix_channel_funcs初始化_GIOChannel->funcs
g_io_create_watch 使用g_io_unix_create_watch函数将channel保存的文件句柄转换为事件源,事件源函数为g_io_watch_funcs
g_io_add_watch 使用g_io_create_watch创建事件源,然后附加到当前主循环的GMainContext
g_io_unix_prepare 获取channel的输入输出buffer的数据状态,并与需要监听的channel事件进行对比
g_io_unix_check 获取channel的输入输出buffer的数据状态,并与channel实际触发的事件进行对比
posted @ 2018-05-25 12:10  银魔术师  阅读(9251)  评论(0编辑  收藏  举报