PulseAudio:スレッドメインループ

スレッドメインループ
前回は poll のメインループを使ったので、今回はスレッドメインループを使って、イベントを待ってみます。

メインループの関数の詳細はこちら
作成など
メインループの作成などは、以下の関数を使います。

pa_threaded_mainloop *pa_threaded_mainloop_new(void);
void pa_threaded_mainloop_free(pa_threaded_mainloop *m);
pa_mainloop_api *pa_threaded_mainloop_get_api(pa_threaded_mainloop *m);
イベントループの開始/停止
int pa_threaded_mainloop_start(pa_threaded_mainloop *m);
void pa_threaded_mainloop_stop(pa_threaded_mainloop *m);

イベントループのスレッドを開始、または停止させます。

pa_threaded_mainloop_stop() の実行前に、ロックは解除しておくこと。

スレッドを開始させた後は、内部のスレッドで、自動的にサーバーからのイベントが受け取られて、処理されます。

クライアント側は、コールバック関数をセットして、各イベントを処理することになりますが、実行されたコールバック関数は、イベントループのスレッド内で実行される形になるので、注意してください。
この時、ロック状態は維持されます。
ロック
void pa_threaded_mainloop_lock(pa_threaded_mainloop *m);
void pa_threaded_mainloop_unlock(pa_threaded_mainloop *m);

イベントループのロックを行うと、スレッド内のイベントループの処理が一時停止します。

メインスレッドで PulseAudio の操作を行う場合は、イベント処理を行わないようにする必要があります。
でないと、不測のタイミングで、何らかのコールバック関数が来る可能性があります。

ロックは再帰的に行われるので、lock と unlock は、対で実行する必要があります。
待つ
void pa_threaded_mainloop_wait(pa_threaded_mainloop *m);
void pa_threaded_mainloop_signal(pa_threaded_mainloop *m, int wait_for_accept);
void pa_threaded_mainloop_accept(pa_threaded_mainloop *m);

スレッドメインループを使う場合、他のスレッドと同期を行う必要があります。

例えば、イベントループのスレッド内で、特定のコールバック関数が実行されて、特定の状況になったら、もう一方のスレッドで処理を進めるという形です。

この場合は、pa_threaded_mainloop_wait() と pa_threaded_mainloop_signal() を使います。

pa_threaded_mainloop_wait() を実行すると、イベントループのスレッドから実行されたコールバック関数内で、pa_threaded_mainloop_signal() が実行されるまで、待ちます。

コールバック関数内で、条件を満たすような状況になった場合は、pa_threaded_mainloop_signal() を実行すると、pa_threaded_mainloop_wait() で待機しているすべてのスレッドにシグナルを送り、wait を終了させます。

これにより、イベントループのスレッド側でトリガーが発生するまで、処理を待つという動作が行えます。

なお、wait を実行する前に、pa_threaded_mainloop_lock() で、ロック状態にしておく必要があります。
イベントを待っている間はロックが解除され、関数が戻る直前に、再度ロックされます。
※コールバック関数は、ロック状態で実行されます。

signal で、wait_for_accept 引数が 0 以外の場合は、pa_threaded_mainloop_accept() と一緒に使います。
pa_threaded_mainloop_accept
pa_threaded_mainloop_accept() は、実行されたコールバック関数に渡された、引数などのデータを、wait から戻ったスレッド側で使用したい時に使います。

まず、コールバック関数内で、pa_threaded_mainloop_signal() の wait_for_accept 引数を、0 以外にして実行します。

wait から戻った後、コールバック関数内のデータを参照し、その後、pa_threaded_mainloop_accept() を実行します。
これにより、accept が実行されるまでは、コールバック内のデータが有効な状態になります。
プログラム
スレッドメインループを使って、サーバーと接続するまで待つプログラムです。

$ cc -o 03-thread 03-thread.c -lpulse

#include <stdio.h>
#include <stdlib.h>
#include <pulse/pulseaudio.h>

pa_threaded_mainloop *ml;
pa_context *ctx;

static void _appfree(void)
{
    pa_context_unref(ctx);
    pa_threaded_mainloop_free(ml);
}

static void _enderr(void)
{
    int err = pa_context_errno(ctx);

    printf("[error] (%d) %s\n", err, pa_strerror(err));
    _appfree();
    exit(1);
}

//コールバック

static void _callback(pa_context *c,void *userdata)
{
    pa_context_state_t state;

    state = pa_context_get_state(c);

    switch(state)
    {
        case PA_CONTEXT_CONNECTING:
            printf(">CONNECTING\n");
            break;
        case PA_CONTEXT_AUTHORIZING:
            printf(">AUTHORIZING\n");
            break;
        case PA_CONTEXT_SETTING_NAME:
            printf(">SETTING_NAME\n");
            break;
        case PA_CONTEXT_READY:
            printf(">READY\n");
            break;
        case PA_CONTEXT_FAILED:
            printf(">FAILED\n");
            break;
        case PA_CONTEXT_TERMINATED:
            printf(">TERMINATED\n");
            break;
        default:
            printf(">state: %d\n", state);
            break;
    }

    //wait を終了させる

    if(state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED)
        pa_threaded_mainloop_signal(ml, 0);
}

int main(void)
{
    pa_context_state_t state;

    ml = pa_threaded_mainloop_new();

    ctx = pa_context_new(pa_threaded_mainloop_get_api(ml), "test-pulse");

    //接続

    printf("connect..\n");

    pa_context_set_state_callback(ctx, _callback, NULL);

    if(pa_context_connect(ctx, NULL, 0, NULL) < 0)
        _enderr();

    //待つ

    pa_threaded_mainloop_lock(ml);
    pa_threaded_mainloop_start(ml);

    while(1)
    {
        state = pa_context_get_state(ctx);

        if(state == PA_CONTEXT_READY || state == PA_CONTEXT_FAILED)
            break;

        pa_threaded_mainloop_wait(ml);
    }

    pa_threaded_mainloop_unlock(ml);
    pa_threaded_mainloop_stop(ml);

    //切断

    if(state == PA_CONTEXT_READY)
        pa_context_disconnect(ctx);

    //解放

    _appfree();

    return 0;
}
解説
やっている事自体は、poll のメインループとあまり変わりませんが、PulseAudio のメインループ処理が、別のスレッド内で自動処理されている点で異なります。

pa_threaded_mainloop_start() を実行した時点で、スレッドが作成され、イベント処理が自動的に始まりますが、その前にロックをしているので、実際にはまだ、イベントループは始まっていません。
実際にイベント処理が実行されるのは、pa_threaded_mainloop_wait() が実行されている間だけです。

connect 後、接続状態が確定していない場合は、pa_threaded_mainloop_wait() を実行して、状態が確定するまで待ちます。
待っている間はロックが解除され、イベントループスレッド内で、イベント処理が行われます。

サーバーからのイベントが処理され、コンテキスト状態変化のコールバック関数が来た時に、READY か FAILED の状態になった場合は、pa_threaded_mainloop_signal() を実行して、シグナルを送ります。
これにより、メインスレッドの wait が終了して、戻ってきます。

今回の場合は、READY または FAILED の状態にならない限り、pa_threaded_mainloop_wait() 関数は戻ってきません。
(ただし、エラー等の理由で戻ってくる可能性もあるので、戻ってきた後に状態のチェックを行うことは必要です)
メインループの違い
poll のメインループは、主に、プログラム内で PulseAudio の処理だけを行う場合に使います。
ただし、プログラム内で独自にスレッドを作成して、その中でメインループを実行させれば、並行処理させることはできます。
こちらの利点は、メインループを自分で処理できるということです。

スレッドのメインループは、PulseAudio のメインループを別スレッドで実行しつつ、メインスレッドで他の処理を行うことができます。
実行環境に左右されずに、同じコードでスレッドの処理を行えるのが利点ですが、メインループは自分で処理できません。

どちらを使うかは、プログラマーの自由です。
なお、シンプル API の場合は、スレッドメインループを使って実装されています。