今更ながらlibeventについて語る(その2)

はじめに

前回の続きです。

今回はイベントループ (event_base) と イベント (event) について語ります。

使い方は libevent-book にもありますが、自分なりに解釈した内容になります。

そもそもイベントループとは

イベント駆動型プログラミング Wikipedia を見てくださいで終わりだが、libevent 風に解説してみる

イベントループ

ループ処理で後述のイベントキューに登録されたイベントを監視し続ける。

イベントがアクティブになった際に後述のコールバック関数を呼び出す。

イベント駆動 ≠ ただのループ処理

イベント

何らかのアクションが発生(ソケットがデータを受信した、GUI の画面上でボタンがクリックされた、タイマーが時間切れになったなど)ことをイベントと広義的に呼ぶらしい。

libevent ではイベントの状態が初期化状態(Initialized)、保留状態(Pending)、アクティブ状態(Activated)の 3 種類があり、以下のようになっている。

  • 初期化状態・・・イベントループによって監視されていない
  • 保留状態・・・イベントループに登録されている(監視されている)が、アクションが発生していない
  • アクティブ状態・・・アクションが発生して、イベントディスパッチャによって後述のイベントハンドラ(コールバック関数)が呼び出され、完了するまでの状態

イベントキュー

libevent でいうと、イベントループに登録された保留状態もしくはアクティブ状態のイベントが登録されている(はず)

イベントハンドラ

コールバック関数のこと。

libevent ではイベントをイベントループに登録する際に予め登録しておく。

libevent の基本的な使い方

libevent の使い方は以下を知れば使えるようになる。

  1. イベントループ (event_base)
    1. イベントループの作成
    2. イベントループの開始
    3. イベントループの停止
  2. イベント (event)
    1. イベントの作成 (初期化)
    2. イベントを保留中にする (イベントループに登録する)
    3. イベントを削除する
    4. イベントの状態をチェック
    5. イベントに関するその他の関数

イベントループ (event_base)

イベントループの作成

libevent でイベントループは event_base 構造体で表現されています。

event_base の定義は以下のようになります。

struct event_base {
    /** Function pointers and other data to describe this event_base's
     * backend. */
    const struct eventop *evsel;
    /** Pointer to backend-specific data. */
    void *evbase;

    /** List of changes to tell backend about at next dispatch.  Only used
     * by the O(1) backends. */
    struct event_changelist changelist;

    /** Function pointers used to describe the backend that this event_base
     * uses for signals */
    const struct eventop *evsigsel;
    /** Data to implement the common signal handelr code. */
    struct evsig_info sig;

    /** Number of virtual events */
    int virtual_event_count;
    /** Maximum number of virtual events active */
    int virtual_event_count_max;
    /** Number of total events added to this event_base */
    int event_count;
    /** Maximum number of total events added to this event_base */
    int event_count_max;
    /** Number of total events active in this event_base */
    int event_count_active;
    /** Maximum number of total events active in this event_base */
    int event_count_active_max;

    /** Set if we should terminate the loop once we're done processing
     * events. */
    int event_gotterm;
    /** Set if we should terminate the loop immediately */
    int event_break;
    /** Set if we should start a new instance of the loop immediately. */
    int event_continue;

    /** The currently running priority of events */
    int event_running_priority;

    /** Set if we're running the event_base_loop function, to prevent
     * reentrant invocation. */
    int running_loop;

    /** Set to the number of deferred_cbs we've made 'active' in the
     * loop.  This is a hack to prevent starvation; it would be smarter
     * to just use event_config_set_max_dispatch_interval's max_callbacks
     * feature */
    int n_deferreds_queued;

    /* Active event management. */
    /** An array of nactivequeues queues for active event_callbacks (ones
     * that have triggered, and whose callbacks need to be called).  Low
     * priority numbers are more important, and stall higher ones.
     */
    struct evcallback_list *activequeues;
    /** The length of the activequeues array */
    int nactivequeues;
    /** A list of event_callbacks that should become active the next time
     * we process events, but not this time. */
    struct evcallback_list active_later_queue;

    /* common timeout logic */

    /** An array of common_timeout_list* for all of the common timeout
     * values we know. */
    struct common_timeout_list **common_timeout_queues;
    /** The number of entries used in common_timeout_queues */
    int n_common_timeouts;
    /** The total size of common_timeout_queues. */
    int n_common_timeouts_allocated;

    /** Mapping from file descriptors to enabled (added) events */
    struct event_io_map io;

    /** Mapping from signal numbers to enabled (added) events. */
    struct event_signal_map sigmap;

    /** Priority queue of events with timeouts. */
    struct min_heap timeheap;

    /** Stored timeval: used to avoid calling gettimeofday/clock_gettime
     * too often. */
    struct timeval tv_cache;

    struct evutil_monotonic_timer monotonic_timer;

    /** Difference between internal time (maybe from clock_gettime) and
     * gettimeofday. */
    struct timeval tv_clock_diff;
    /** Second in which we last updated tv_clock_diff, in monotonic time. */
    time_t last_updated_clock_diff;

#ifndef EVENT__DISABLE_THREAD_SUPPORT
    /* threading support */
    /** The thread currently running the event_loop for this base */
    unsigned long th_owner_id;
    /** A lock to prevent conflicting accesses to this event_base */
    void *th_base_lock;
    /** A condition that gets signalled when we're done processing an
     * event with waiters on it. */
    void *current_event_cond;
    /** Number of threads blocking on current_event_cond. */
    int current_event_waiters;
#endif
    /** The event whose callback is executing right now */
    struct event_callback *current_event;

#ifdef _WIN32
    /** IOCP support structure, if IOCP is enabled. */
    struct event_iocp_port *iocp;
#endif

    /** Flags that this base was configured with */
    enum event_base_config_flag flags;

    struct timeval max_dispatch_time;
    int max_dispatch_callbacks;
    int limit_callbacks_after_prio;

    /* Notify main thread to wake up break, etc. */
    /** True if the base already has a pending notify, and we don't need
     * to add any more. */
    int is_notify_pending;
    /** A socketpair used by some th_notify functions to wake up the main
     * thread. */
    evutil_socket_t th_notify_fd[2];
    /** An event used by some th_notify functions to wake up the main
     * thread. */
    struct event th_notify;
    /** A function used to wake up the main thread from another thread. */
    int (*th_notify_fn)(struct event_base *base);

    /** Saved seed for weak random number generator. Some backends use
     * this to produce fairness among sockets. Protected by th_base_lock. */
    struct evutil_weakrand_state weakrand_seed;

    /** List of event_onces that have not yet fired. */
    LIST_HEAD(once_event_list, event_once) once_events;

};
  • event_base = イベントループ
  • イベントの共通設定やイベントループの設定を event_config を使用することで**_event_base_** に設定できる。
  • epoll, kqueue, evport, /dev/poll, poll, select, winsock の中から最適なものを使用する

event_base は以下の関数のいずれかを使用することで生成する。

struct event_base * event_base_new(void);
struct event_base *event_base_new_with_config(const struct event_config *cfg);

基本的には event_base_new で問題ないが、細かい設定を行いたい場合は以下のように event_config を使用して event_base を生成する。

// event_configの生成
// イベントに関する共通設定を行う
struct event_config *cfg = event_config_new();
if (!cfg) {
    abort();
}

// 一連の機能のすべてを提供できないバックエンドを使用しないように指示
// この場合、edge-triggered IOをサポートしていない場合abortする
if (event_config_require_features(cfg, EV_FEATURE_ET) < 0) {
    abort();
}

// 低い優先度が設定されたイベントの実行タイムアウト時間や回数を制限することで
// 高い優先度が設定されたイベントが実行されない(または優先されない)ことを防ぐ
// この場合、100ms以上もしくは1つのイベントが実行されたとき、優先度が高いイベントに対して再スキャンを行うように、優先度2以上のイベントに適用する。
struct timeval notmorethen = { 0, 100000 };
if (event_config_set_max_dispatch_interval(cfg, ¬morethen, 1, 2) < 0) {
    abort();
}

// event_baseに関する設定
// EVENT_BASE_FLAG_NOLOCK を設定すると、
// event_base がスレッドセーフで無くなるが、ロックコストがなくなるためパフォーマンスが向上する。
// EVENT_BASE_FLAG_PRECISE_TIMER を設定すると、より高精度なタイマーを使う。
// EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST を設定すると、
// epollを使用する場合、チェンジリストベースのバックエンドを使用することは安全である(つまり積極的に使用する?)と伝える。
// おそらく不必要なシステムコールを回避できるため、パフォーマンスが向上する。
if (event_config_set_flag(cfg,
    EVENT_BASE_FLAG_NOLOCK|
    EVENT_BASE_FLAG_PRECISE_TIMER|
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST) < 0) {
    abort();
}

// event_configを適用したevent_baseを作成
event_base *ev_base;
if (ev_base = event_base_new_with_config(cfg) < 0) {
    abort();
}

// event_configの解放
event_config_free(cfg);

他にも event_config を使用しないイベント(ループ)の設定として、

共通タイムアウトの設定 event_base_init_common_timeout 関数や 優先度の個数を設定する event_base_priority_init 関数などがある。

イベントループの開始

イベントループの作成、初期化が完了したら  event_base_loop 関数もしくは、 event_base_dispatch 関数を使用してイベント監視するループを開始する。

event_base_dispatch 関数は内部で  event_base_loop 関数の第二引数を 0(フラグを何も設定しない) で呼び出している (のでフラグを設定しない場合は event_base_dispatch 関数を使用すれば良い)

#define EVLOOP_ONCE             0x01
#define EVLOOP_NONBLOCK         0x02
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04

int event_base_loop(struct event_base *base, int flags);
int event_base_dispatch(struct event_base *event_base);

イベントループの停止

ループを終了する場合は以下の関数を呼ぶ。

int event_base_loopexit(struct event_base *base,
                        const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);

event_base_loopexit の場合

第二引数で指定時間後に終了することを指定できる。
アクティブなイベントのコールバックを実行している場合、 event_base は引き続き実行され、すべて実行されるまで終了しない。
event_base_loopbreak の場合

アクティブなイベントのコールバックを実行している場合、現在処理中のイベントを終了した直後に終了する。

補足


イベントループは以下のような擬似コードで表される。

libevent-book 3. Working with an event loop にも書いてあるが一応

while (event_baseにイベントが登録されている、または EVLOOP_NO_EXIT_ON_EMPTY が設定されている) {

    if (EVLOOP_NONBLOCK が設定されている、または いくつかのイベントがアクティブ)
        登録されたイベントがトリガーされた場合は、アクティブにマーク。
    else
        少なくとも1つのイベントが発生するまで待ってから、それをアクティブにマーク。

    for (p = 0; p < n_priorities; ++p) {
        if (優先度pのイベントがアクティブ) {
            優先度pのイベントを全て実行する。
            break; /* Do not run any events of a less important priority */
        }
    }

    if (EVLOOP_ONCE が設定されている、または
        EVLOOP_NONBLOCK  が設定されている)
        break;
}

イベント(event)

イベントの作成 (初期化)

libevent でイベントは event  構造体で表現されている。

event  の定義は以下のようになります。

struct event {
    struct event_callback ev_evcallback;

    /* for managing timeouts */
    union {
        TAILQ_ENTRY(event) ev_next_with_common_timeout;
        int min_heap_idx;
    } ev_timeout_pos;
    evutil_socket_t ev_fd;

    struct event_base *ev_base;

    union {
        /* used for io events */
        struct {
            LIST_ENTRY (event) ev_io_next;
            struct timeval ev_timeout;
        } ev_io;

        /* used by signal events */
        struct {
            LIST_ENTRY (event) ev_signal_next;
            short ev_ncalls;
            /* Allows deletes in callback */
            short *ev_pncalls;
        } ev_signal;
    } ev_;

    short ev_events;
    short ev_res;       /* result passed to event callback */
    struct timeval ev_timeout;
};

libevent で言うイベントとは基本的に以下の 4 つのアクションのことを指す

  1. ディスクリプタ(ソケット、ファイルなど)に対する読み書きがあった時
  2. シグナル発生時
  3. タイムアウト発生時
  4. ユーザーがトリガーするイベント( event_active 関数)

また、前で述べたとおりイベントの状態は初期化状態(Initialized)、保留状態(Pending)、アクティブ状態(Activated)の 3 種類があり、以下のようになっている。

  • 初期化状態・・・event_new or event_assign 関数で初期化した状態
  • 保留状態・・・event_add 関数でイベントループ (event_base) に登録された状態、かつアクションが発生していない状態
  • アクティブ状態・・・イベントループに登録されていて、かつ上記のアクションのいずれかが発生して、イベントディスパッチャによって後述のイベントハンドラ(コールバック関数)が呼び出され、完了するまでの状態

また、以下の関数を使用することで初期化状態のイベントを作成することができる。

#define EV_TIMEOUT      0x01
#define EV_READ         0x02
#define EV_WRITE        0x04
#define EV_SIGNAL       0x08
#define EV_PERSIST      0x10
#define EV_ET           0x20

typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

struct event *event_new(struct event_base *base, evutil_socket_t fd,
    short what, event_callback_fn cb,
    void *arg);
void event_free(struct event *event);

int event_assign(struct event *event, struct event_base *base,
    evutil_socket_t fd, short what,
    void (*callback)(evutil_socket_t, short, void *), void *arg);

event_new は内部で calloc ( mm_calloc )を呼ぶため、ヒープ上に* sizeof(event)* 分確保する。

ヒープ上にメモリを確保するので、メモリを開放する際には必ず event_free 関数で削除する必要がある。(呼び出さないとメモリリークします)

event_assign は引数で初期化するイベントのポインタを指定できるため、ヒープ上で確保する必要がない。

公式レポジトリの Doxygen から

この方法を使用すると、イベント構造のサイズが異なる Libevent の他のバージョンとのバイナリ互換性が損なわれる可能性があります。

これらは非常に小さいコストであり、ほとんどのアプリケーションで重要ではありません。イベントのヒープ割り当てに重大なパフォーマンス上のペナルティが発生していることが分かっていない限り、event_new()を使用することに固執する必要があります。event_assign()を使用すると、ビルドしようとしているイベント構造より大きなイベント構造を使用している場合、Libevent の将来のバージョンでは診断が困難になることがあります。

公式のソース生成ドキュメントを見る限りでは event_new を推奨している。

フラグを設定するとどのようなときにイベントハンドラを呼ぶか設定することができる。

イベントを保留中にする (イベントループに登録する)

作成したイベントを初期化状態から保留状態にするには、event_add 関数を呼ぶ。

int event_add(struct event *ev, const struct timeval *timeout);

イベントを削除する

例えば、 通常イベントハンドラ処理後にアクティブ状態のイベントはイベントループから登録解除される(初期化状態になる)。

しかし、前述の event_new もしくは event_assign 関数で EV_PERSIST を設定すると、そのイベントは永続的なイベントとして扱われ、イベントハンドラ処理後は保留状態となり再度アクティブ状態になるのを待ち続ける。

例えば HTTP のサーバーとかは基本的にプロセスが終了するまでソケットディスクリプタがアクティブ状態になるまで監視し続ける。そのためこのフラグを初期化時に設定することで、イベントハンドラ処理後のイベントを event_add 関数でイベントループに再登録する必要がなくなる。

しかし、反対に永続的なイベントや保留中のイベントを初期化状態に戻す (イベントループから登録解除する) には event_del 関数を呼び出す必要がある。

int event_del(struct event *ev);
int event_remove_timer(struct event *ev);

event_remove_timer 関数は保留中のイベントからタイムアウトを完全に削除することができます。

event_add 関数でタイムアウトの値が設定されていない場合、 event_remove_timer 関数は効果はない。

イベントのフラグとして EV_TIMEOUT のみ設定された、つまり純粋なタイマーイベントとして初期化されたイベントに対して呼び出された event_remove_timer 関数は event_del 関数と同じ効果を持ちます。

イベントの状態をチェック

以下の関数を使用することでイベントに関する情報を取得することができる。

int event_pending(const struct event *ev, short what, struct timeval *tv_out);

#define event_get_signal(ev) /* ... */
evutil_socket_t event_get_fd(const struct event *ev);
struct event_base *event_get_base(const struct event *ev);
short event_get_events(const struct event *ev);
event_callback_fn event_get_callback(const struct event *ev);
void *event_get_callback_arg(const struct event *ev);
int event_get_priority(const struct event *ev);

void event_get_assignment(const struct event *event,
        struct event_base **base_out,
        evutil_socket_t *fd_out,
        short *events_out,
        event_callback_fn *callback_out,
        void **arg_out);

イベントに関するその他の関数

一応紹介

struct event *event_base_get_running_event(struct event_base *base);
int event_base_once(struct event_base *, evutil_socket_t, short,
  void (*)(evutil_socket_t, short, void *), void *, const struct timeval *);
void event_active(struct event *ev, int what, short ncalls);

純粋なタイマーイベントや、シグナル検知のイベントを作成したい場合は以下のマクロ関数を用いることもできる。

#define evtimer_new(base, callback, arg) \
event_new((base), -1, 0, (callback), (arg))
#define evtimer_add(ev, tv) \
event_add((ev),(tv))
#define evtimer_del(ev) \
event_del(ev)
#define evtimer_pending(ev, tv_out) \
event_pending((ev), EV_TIMEOUT, (tv_out))

#define evsignal_new(base, signum, cb, arg) \
event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)
#define evsignal_add(ev, tv) \
event_add((ev),(tv))
#define evsignal_del(ev) \
event_del(ev)
#define evsignal_pending(ev, what, tv_out) \
event_pending((ev), (what), (tv_out))

他にも定義されている関数があるので include/event2/event.h を確認もしくは Doxygen でドキュメントを生成して確認することをおすすめする。

次回は evhttp に関して書きます (これが書きたかった)