081221

|


同期プリミティブを実装しました。無難にはセマフォ、あるいは条件変数なの
だけど、敢えてモニタにしてみた。使ったことないし、興味がある。

UNIXカーネル内部解析-キャッシュとマルチプロセッサの管理 (プロフェッショナルコンピューティングシリーズ) カート シメル Curt Schimmel 岩本 信一 12.2章のモニタの説明から書いてみました。
この本は名著中の名著。キャッシュまわりをいじるなら必読。
SIMPLEQ_HEAD (tc_queue, thread_control);

struct monitor
{
  enum monitor_state state;
このステートはデバッグ用です。

  int lock;
リソースを含む区画のロックです。

  struct tc_queue lock_queue;
ロックが取得できなかった際にスリープに入るキューです。

  struct tc_queue event_queue;
ロックを取得後、wait()でイベントを待つ際のスリープキューです。

} monitor[MONITOR_MAX];

void __monitor_reset (struct monitor *);

void
monitor_init ()
{
  int i;

  for (i = 0; i < MONITOR_MAX; i++)
    __monitor_reset (monitor + i);
}

void
__monitor_reset (struct monitor *m)
{

  m->state = MON_FREE;
  SIMPLEQ_INIT (&m->lock_queue);
  SIMPLEQ_INIT (&m->event_queue);
}

struct monitor *
monitor_allocate ()
{
  struct monitor *m = NULL;
  int i;
  int s = intr_suspend ();

  for (i = 0; i < MONITOR_MAX; i++)
    {
      if (monitor[i].state == MON_FREE)
	{
	  m = monitor + i;
	  break;
	}
    }
  intr_resume (s);

  return m;
}

void
monitor_deallocate (struct monitor *m)
{
  int s = intr_suspend ();

  __monitor_reset (m);
  intr_resume (s);
}

void
monitor_enter (struct monitor *m)
{
  int s = intr_suspend ();

  while (m->lock)
    {
      // add to lock wait queue.
      SIMPLEQ_INSERT_TAIL (&m->lock_queue, current_thread, tc_monitor_lock);
      thread_sleep (current_thread->id);

ロックがとれなければ、他のスレッドがモニタから出る(monitor_exit())か、イベント
待ち(monitor_wait())になるかまで寝ます。

    }
  m->lock = 1;
  m->state = MON_LOCKED;

  intr_resume (s);
}

モニタの定義によってmonitor_wait,monitor_signalはmonitor_enterから
monitor_exit までの間だけしか実行してはいけない。

(???これによって寝ようとする時に起こしにかかられるレースを解決できる???)
モニタの区間のロックをとれなければ一切のことができないのがモニタの特徴。

void
monitor_wait (struct monitor *m, int event)
{
  int s = intr_suspend ();

モニタは過去のイベントを記憶しないので(そういう定義)、wait操作は常に寝る。
  // monitor wait operation always blocked.
  SIMPLEQ_INSERT_TAIL (&m->event_queue, current_thread, tc_monitor_event);
  current_thread->monitor_wakeup_request = 0;
  current_thread->monitor_event = event;
  m->state = MON_WAIT;

  m->lock = 0;

ロックを解除。ここで他のスレッドがモニタに入ることができる。新しくモニ
タに入ってきてwaitで寝るかもしれないし、シグナルを送ってくるかもしれな
い。新しく入ってきて寝るスレッドはこの後にスケジューリングされるし、シ
グナルを送ってくるスレッドなら、そのスレッドがmonitor_exit ()でモニタを
抜けるまで実行は再開されない。(monitor_signalではロックを明け渡すことはないので)
  thread_sleep (current_thread->id);

他のスレッドのsignal操作によって起こされた。シグナル側が新しく送ろうと
してもスケジューリングが既に決まっているので、自分がmonitor_exitするま
では何もできない。(monitor_enterで待ちになる)

  current_thread->monitor_wakeup_request = 0;
  SIMPLEQ_REMOVE (&m->event_queue, current_thread, thread_control,
		  tc_monitor_event);

  m->lock = 1;
ロックを保持したまま抜けます。

  intr_resume (s);
}

void
monitor_signal (struct monitor *m, int event)
{
  struct thread_control *tc;
  int s = intr_suspend ();

  // monitor don't memorize operation.
  SIMPLEQ_FOREACH (tc, &m->event_queue, tc_monitor_event)
    {
      if (tc->monitor_event & event)
	{
	  tc->monitor_wakeup_request = 1;
	  // dont' wakeup here.
	  //	  SCI_PRINTF ("monitor signal->%d\n", tc->id);
	}
    }

イベントの内容に対して寝ているスレッドがあれば、それを全部起こす予定に
します。モニタの中には1スレッドしか入れないので、この段階では起こされま
せん。

「モニタに入るのを待っているスレッドは起こされたスレッド全てが終了するまで
待たされる。」のを確定するため、レディキューに先に登録しておく。

どのスレッドも待っていなければ、なにもしません。(先にイベントがキューされ
ているから、monitor_waitが寝ずに抜けるというようなことはない。)

  m->state = MON_SIGNAL;

  intr_resume (s);
}

void
monitor_exit (struct monitor *m)
{
  struct thread_control *tc;
  int s = intr_suspend ();

  // wakeup scheduled thread if any.
  // monitor assures wakeuped thread can handle resource.
  SIMPLEQ_FOREACH (tc, &m->event_queue, tc_monitor_event)
    {
      if (!tc->monitor_wakeup_request)
	continue;
モニタを抜けるにあたってシグナルによって起こされる予定のスレッドを探します。

      thread_wakeup (tc->id);
まず最初にシグナルで起こされる予定だったスレッド達を起こします。とはい
えまだ自分がモニタ権をもっているのでレディキューにつながれるだけ。
    }

モニタのセマンティクスはシグナルによって起こされるスレッドが確実に次の
モニタの権利を得ることができること。(新しくモニタに入ろうとするスレッド
は後まわし)これで、起きたはいいけれど、ちょっとした隙に新参物にとられて
しまうことを防げる。

  while ((tc = SIMPLEQ_FIRST(&m->lock_queue)) != NULL)
    {
      SIMPLEQ_REMOVE_HEAD(&m->lock_queue, tc_monitor_lock);
      thread_wakeup (tc->id);
    }
新しくモニタに入ろうとしているスレッドがあれば、シグナルで起こされるスレッド
の後につなぎます。

  m->lock = 0;
  m->state = MON_UNLOCKED;
モニタから出た。

  intr_resume (s);
}

これで本当にいいのだろうか。使ったこともないのを実装するのは無謀だった かも。でもなんかこれ便利そうです。ロックの取得のオーバーヘッドが多きい けど、条件変数を確実に使う場合ならこっちの方がよさそう。ただの待ちあわ せならセマフォの方がよさそう。
テストはこんな感じで送信のスレッドを一つ。受信のスレッドを三つ。受信は スレッドIDのビットが立っているイベントを受ける。
struct
{
  struct monitor *mon;
  int data;
} button;
void button_put (int, int);
int button_get (int);

void
thread_test ()
{
  int id;

  button.mon = monitor_allocate ();

  thread_create (button_get_thread, &id);
  thread_start (id);
  thread_create (button_get_thread, &id);
  thread_start (id);
  thread_create (button_get_thread, &id);
  thread_start (id);

  thread_create (button_put_thread, &id);
  thread_start (id);
}

void
button_put (int data, int channel)
{

  monitor_enter (button.mon);
  button.data = data;
  monitor_signal (button.mon, channel);
  monitor_exit (button.mon);
}

int
button_get (int channel)
{
  int data;

  monitor_enter (button.mon);
  monitor_wait (button.mon, channel);
  data = button.data;
  monitor_exit (button.mon);

  return data;
}

void
button_put_thread ()
{
  int id;
  int channel = 0;

  id = thread_id ();
  SCI_PRINTF ("thread %d(sender):\n", id);
  while (/*CONSTCOND*/1)
    {
      int button;
      if ((button = push_switch ()) != 0)
	{
	  channel = button == PS_1 ? 1 : 2;
	  SCI_PRINTF ("thread %d: SEND %d channel %d\n", id, button, channel);
	  button_put (button, channel);
	  //	  thread_switch ();
テスト用にタイマ割り込みでラウンドロビンさせているのでここでスイッチしなくて
いい。そうでない時は受信側に明示的にスイッチする。
	}
    }
}

void
button_get_thread ()
{
  int id;

  id = thread_id ();
  SCI_PRINTF ("thread %d(receiver):\n", id);
  while (/*CONSTCOND*/1)
    {
      int button = button_get (id);
      SCI_PRINTF ("thread %d: RECV %d\n", id, button);
    }
}
1476 lines transferred in 6 seconds 
!
  Top Address=80000
  End Address=FE003
: g
bss: 0x82d10-0x8389c
MCU mode: 1
thread_init 0: 830cc-82fd0
thread_init 1: 831cc-830d0
thread_init 2: 832cc-831d0
thread_init 3: 833cc-832d0
thread_init 4: 834cc-833d0
thread_init 5: 835cc-834d0
thread_init 6: 836cc-835d0
thread_init 7: 837cc-836d0
thread create 1: pc=81834 sp=831cc
thread create 2: pc=81834 sp=832cc
thread create 3: pc=81834 sp=833cc
thread create 4: pc=817b0 sp=834cc
thread 1(receiver):
thread 2(receiver):
thread 3(receiver):
thread 4(sender):
thread 4: SEND 2 channel 2
thread 2: RECV 2
thread 3: RECV 2
thread 4: SEND 4 channel 1
thread 1: RECV 4
thread 3: RECV 4
thread 4: SEND 8 channel 2
thread 2: RECV 8
thread 3: RECV 8
なんとか想定通りに動いていそうだけれど...どうなんだ? これは。あってるの だろうか? とんでもない勘違いをしていそうで怖い。使ってみながら確認かな。 ソフト側の開発環境整備もあともうちょっと!