2007年5月アーカイブ

概要: 複数のスレッドが動いていると、どのような順番でそれぞれの処理が実行されるかという事は、予測が困難なものですね。 先日、DirectSound周りの処理を改良していた時、スレッドを二つ作り、メインスレッ...

複数のスレッドが動いていると、どのような順番でそれぞれの処理が実行されるかという事は、予測が困難なものですね。

先日、DirectSound周りの処理を改良していた時、スレッドを二つ作り、メインスレッドでは次のような処理を行わせていました(話を単純にする為にある程度簡略化しています)。

// メインスレッド(通常の優先度)で
private void PlayButton_Click( object sender, EventArgs e ) {
  // ……
  // OggVorbisDecoderは、ISoundDecoderを実装するクラス
  this.sound = new StreamingSound(
    new OggVorbisDecoder( new FileStream( this.sounds[ 0 ] ) )
  );
  for ( int i = 1 ; i < sounds ; i++ )
    // StreamingSound.AddFollowingDecoderは、
    // 部分ループ再生の為に後続のデコーダを追加するメソッド
    this.sound.AddFollowingDecoder(
      new OggVorbisDecoder( new FileStream( this.sounds[ i ] ) )
    );
  // それなりに長い処理(空行抜きで15行分程度)
  this.sound.Play();
  // ……
}

// メインスレッドから呼び出される、StreamingSoundのコンストラクタ
public StreamingSound( ISoundDecoder soundDecoder ) {
  this.soundDecoder = soundDecoder;
  // ……
  this.refreshingThread = new Thread( this.RefreshingThread );
  this.refreshingThread.Start();

  return;
}

// メインスレッドから呼び出される、StreamingSoundのメソッド
public void Play() {
  // this.bufferは、Microsoft.DirectX.DirectSound.SecondaryBuffer
  if ( this.buffer.Status.Playing )
    return;

  // this.decoderLockは、単なるobjectのインスタンス
  lock ( this.decoderLock )
    this.buffer.Play( 0, BufferPlayFlags.Looping );

  return;
}

そして、セカンダリバッファの内容を更新するRefreshingThreadでは、次のようにしていました。

// バックグラウンドスレッド(通常の優先度)で
private void RefreshingThread() {
  // 変数宣言(代入含めて、5つ程)……
  // まだPlayが呼び出されては困る(ゴミが再生される恐れがある)
  lock ( this.decoderLock )
    // バッファの前半にPCMデータを書き込む
    this.WriteHalfBuffer( 0, 0 );
  // 準備完了、もうPlayを呼び出しても大丈夫
  // ……
}

lock ( this.decoderLock )というのは、クリティカルセクションを作る為に導入したものです。lock文のブロックの中に何れかのスレッドが進入すると、他のスレッドでは、現在クリティカルセクションに進入しているスレッドがそのオブジェクトの所有権を手放さない限り(つまりlock文を抜けない限り)、同じオブジェクトを使ってlock文の中に入る事ができなくなります。ここでは僕は、二つのスレッドの優先度が同じで、RefreshingThreadをthis.refreshingThread.Start()で開始させた時からの作業量はどう見てもバックグラウンドスレッドの方が少ないので、WriteHalfBufferが先に呼ばれるものと思っていました。実際Debugビルドではそうだったようで、再生してみても、サウンドの頭から綺麗に再生されていました。

ところが、Releaseビルドで作られた実行ファイルでは、どうも初回の再生だけ汚くなるのです。ノイズが聞こえる訳ではないですが、どうも頭の部分が切れている。初回の再生(初めて「再生」ボタンを押した時)と二回目以降の再生で違うのは、StreamingSoundを作り直すかどうかだけ。つまり、上記のソースコードで一番上の辺りの処理が怪しいのです。色々調べてみましたが、ふと気になって、StreamingSound.Playの中と、RefreshingThreadのWriteHalfBufferの前後で、System.DateTime.Now.Ticksを使って、処理が実行された時間を調べてみました。StreamWriterによって書き出されたログを見てみると……案の定、Playが先に呼ばれています。少し後れてRefreshingThreadのWriteHalfBufferが実行されていたようです。そりゃー、頭が切れる訳ですね。再生開始時点では何もバッファに書き込まれていない訳ですから。雑音が聞こえなかったのは運が良かったのでしょう。

さて、ではどう解消すべきか。lock文は、クリティカルセクションを作る事はできますが、スレッドの処理の実行順序を支配する事はできません。ミューテックス(System.Threading.Mutex)も同様ですし、モニタ(System.Threading.Monitor)はlock文の実体なので、書き換えた所で意味がありません。また、Releaseビルドでは問題が起こるけれどもDebugビルドでは起こらないから、といってReleaseビルドの実行ファイルを使わないようにするのも面白くありません。それに、Debugビルドだって、きちんと動いているように見えるだけで、その動作は「たまたま」上手く行っているのでしょう。確実に実行順序を支配する機構が欲しい所です。

これに打って付けなのは、イベントでしょう。と言ってもC#の言語機構のeventキーワードやdelegateを使う方ではなく、Win32 APIにもあるような、スレッドの同期を実現するイベントの方です。.NET Frameworkでは、System.Threading.AutoResetEventやSystem.Threading.ManualResetEventがそれに当たります。今回は特にマニュアルリセットが必要とされるものでもないので、AutoResetEventを使って同期してみました。主な変更は次の3箇所です。

// StreamingSoundのコンストラクタ
public StreamingSound( ISoundDecoder soundDecoder ) {
  this.soundDecoder = soundDecoder;
  // ……
  // 最初は非シグナル状態にするので、falseを渡す
  this.decoderReady = new AutoResetEvent( false );
  // ……
  this.refreshingThread = new Thread( this.RefreshingThread );
  this.refreshingThread.Start();

  return;
}

// StreamingSound.RefreshingThread
private void RefreshingThread() {
  // 変数宣言……
  // バッファの前半にPCMデータを書き込む
  this.WriteHalfBuffer( 0, 0 );
  this.decoderReady.Set();
  // ……
}

// StreamingSound.Play
public void Play() {
  if ( this.buffer.Status.Playing )
    return;

  this.decoderReady.WaitOne();
  this.buffer.Play( 0, BufferPlayFlags.Looping );

  return;
}

イベントには、ミューテックスなどと同じように、シグナル状態と非シグナル状態という二つの状態があります。シグナル状態ではWaitOneなどのメソッドは成功し、次に進む事ができます。非シグナル状態ではWaitOneなどのメソッドは成功する事は無く、次に進む事はできません(無理矢理進んだりしても、同期に失敗しているのですから望む結果は得られないでしょう)。シグナル状態には、コンストラクタでtrueを渡す、Setメソッドを呼び出す、などの操作をする事で変化させる事ができます。非シグナル状態には、コンストラクタでfalseを渡す、Resetメソッドを呼び出す、WaitOneなどの待機関数を成功させる、などの操作で変化させられます。

StreamingSound.Playで呼び出しているWaitOne関数は引数がありません。引数無しでこの関数を呼び出すと、そのイベントがシグナル状態になるまで無限に待機します。コンストラクタではこのイベントは非シグナル状態にされているので、バックグラウンドスレッドのRefreshingThreadがSetしない限り、メインスレッドはサウンドの再生を開始しないという訳です。

……久し振りにプログラミングに関する覚え書きを書きました。

概要: メールと言えば、今までスパムの全然来なかったw4ard@s26.xrea.comにも、このサイトのページがGoogleに拾われたり、w3c-translators@w3.orgのアーカイブが拾われたり...

メールと言えば、今までスパムの全然来なかったw4ard@s26.xrea.comにも、このサイトのページがGoogleに拾われたり、w3c-translators@w3.orgのアーカイブが拾われたりしているせいか、少し(本当に少し)スパムが来始めました。ウェブサイトの作成者さんの中には、メールアドレス収集ロボットに拾われないように直接記載する事を避ける人もいるようですが、悪意を持った人間には無力です(そう言う人が来るのかどうかは知らないですけど)。それに、mailtoプロトコル(なのかな?)でリンクを張って手軽に送れるようにしていないと、メールを送信しようとしてくれた人も面倒に思って止めてしまうかも知れません。それに、ウェブページにメールアドレスを記載しなくても、知らない所で名簿屋にアドレスが売られているなんて事もあるので(うちのプロバイダのメールアドレスも、未公開にも拘わらず一時期大量にスパムに遭ったし)、直前に防壁が欲しい所です。

なーんて、何だか通信販売の前置きみたいな繋ぎを書きましたが、要はPOPFileを紹介したかっただけです。有名っぽいので既に御存知の方もいるでしょうが、ベイズ理論を利用してメールのフィルタリングを行う中継サーバソフトです。簡単な手順の学習を少しする事によって、普通のメールとスパムを分別したり、もっと高度な分類を行う事もできます。ちなみに、僕のPOPFileの記録では、1062通のメールの内、分類を間違えたのは17通だけ(それも殆ど最初の内だけ)です。精度は98.39%。これでスパムを無視し続けていたせいか、プロバイダのメールアカウントで一時期酷かったスパムも、最近は殆ど来なくなりました。

ベイズ理論てのは、あれですね……「Aという単語が見つかった時、そのメールがスパムである確率」を「メールがスパムであった時、そのAという単語が含まれていた確率」などに置き換えられるような感じの(数式出すのは面倒くさい)。単純ですが、ここまで便利なものだったとは。

しかしまあ、何と言うか……。これ、どこがプログラミングに関する覚え書きがメインのウェブログなんだか。

概要: ISO-2022-JP以外のエンコーディングも使えるんですね。ちょっとOutlookのオプション設定をいじっていたら、エンコーディングをUTF-8にする事もできたので、名前の訂正依頼をしておきました。...

ISO-2022-JP以外のエンコーディングも使えるんですね。ちょっとOutlookのオプション設定をいじっていたら、エンコーディングをUTF-8にする事もできたので、名前の訂正依頼をしておきました。Coralieさん、手数をおかけしてごめんなさい……(ここで言ってもしょうがない)。最初は、Extensible Markup Language (XML) 1.1 (第二版)の冒頭の部分で、この翻訳版は長谷川 宏聡(Hiroaki Hasegawa……(後略)なんて書いていたので、「は」も紛れ込んでしまったのでしょう。これを受けて(最初からやってれば良かったんですけど)、少し判りやすく構成し直しました。他にも、内容に関する間違いとか、解りにくい所とかありましたら、是非w4ard@s26.xrea.comまでお知らせ下さい。随時歓迎します。このエントリーへのコメントでも構いません。

このアーカイブについて

このページには、2007年5月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2007年4月です。

次のアーカイブは2007年7月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。