移動: トップ > ラボ > プログラミングのアイディア > wait文を導入してコードを止める
yield文でイベントを待機する
履歴:
統計:
目次:
このページの最終更新日: 2011/06/22
提案日: 2009/02/13

※本記事ではプログラミングに関するアイディアを紹介しています。これらは特に記載がない限り、まだ実現されていないものです。これらのアイディアをぜひ実現していただける方をお待ちしております。

wait文を導入してコードを止める

概要

プログラム言語に構文「wait イベント名」を新しく導入して、このwait文のある行でコードの実行を一時停止して待機する、といったアイディアです。コードの途中で実行の流れを簡単に止めることが出来るようになります。

いろいろなプログラムを開発する中で、構文「wait イベント名」が使えればもっと分かりやすくコードが書けると感じる場面が多くあります。そのいくつかを以降で紹介します。

背景

Verilogの@(変数名)で手軽にコードを止める

Verilog言語の文法には@キーワードがあります。VerilogはFPGAなどICチップのデジタル回路を設計する言語で、その中に@(変数名)といった書き方があります。この@(変数名)を使うと、指定した変数が条件を満たすまでその行でコードの実行が待機されます。例えば、次のようにコードの中で@(posedge x)と置くと、このコードの動作は、処理Aを実行してから、@(posedge x)の部分で変数xが0から1に変化するまで待機し、変化すれば次の処理Bを実行します。

    ...処理A...
    @(posedge x);
    ...処理B...
@(変数名)を使ってコードの実行を一時停止して待機する

これはプログラム的な観点から見ると、他からのイベントをその行で待機する機能と考えられます。通常プログラムは上から下へ順次コードが実行されるので、イベントを待機するためにコードの途中で止まることはできません。もし途中で止めようとするなら、2つのスレッドを用意してシグナルでスレッドを止めておくなど、非同期処理特有の複雑なコードを書かなくてはいけません。

そのため、Verilogの@(変数名)で簡単にイベントを待機できる機能はとても便利です。通常のコードの中に@(変数名)を1行入れるだけで手軽にコードを止めることができます。そこで、他のいろいろな言語に同じ役割を果たす構文「wait イベント名」を導入して、他の言語でも簡単にコードを止めてイベントを待機できるようにする、といったアイディアに至りました。

いろいろな場面で威力を発揮する

言語に構文「wait イベント名」を新しく導入するアイディアに至ってから、いろいろなプログラムを開発する中で、構文「wait イベント名」が使えればもっと分かりやすくコードが書けると感じる場面が多くありました。そのいくつかを以降で紹介します。これらのことから構文「wait イベント名」が言語に必要なことをより強く感じるようになりました。

背景→Verilogである監視信号が条件を満たすまで待機する@がとても便利jQueryでコールバック関数を使ってイベントを待つコードが多数になり、この書き方だとコードが複雑になり読みにくい(if分岐やfor繰り返しで流れを制御しようと思うと複雑化)→@のような文でイベントを待てるときれいに書けると思うそこで「wait+イベント名」文でイベントを待機するコードの書き方
■2009/02/13=wait文の提案、2009/10/01=wait文の機能をyield文で擬似実現

どんなもの?

構文「wait イベント名」を使ってコードの実行を一時停止して待機する

プログラム言語に構文「wait イベント名」を新しく導入して、このwait文のある行でコードの実行を一時停止して待機する、といったアイディアです。構文「wait イベント名」は現在のプログラム言語にまだない機能で、新しく構文を作って取り入れます。コードの中で構文「wait イベント名」を書くと、このwait文に指定したイベントが発生するまで、その行でコードの実行が待機されます。

例えば、次のようにコードの中で「wait event;」といった感じでwait文を書くと、このコードの動作は、処理Aを実行してから、「wait event;」の部分でeventが発生するまで待機し、発生すれば次の処理Bを実行します。

    ...処理A...
    wait event;
    ...処理B...
構文「wait イベント名」を使ってコードの実行を一時停止して待機する

このように構文「wait イベント名」があれば、コードの途中で実行の流れを簡単に止めることが出来るようになります。単純に構文を導入しただけのように見えますが、言語に構文「wait イベント名」があると、かなりパワフルな威力を発揮します。次に、威力を発揮するいろいろな場面を紹介します。

いろいろなコードの中で威力を発揮できる

構文「wait イベント名」を使えば、コードがかなりすっきり書ける場面がたくさんあります。複数回起こるイベントの一連の処理をコードに書く場合や、非同期で処理して相手側の終了を待つコードを書く場合など、構文「wait イベント名」はいろいろな場面で威力を発揮します。下の表に、コードがスマートに書ける場面の例をまとめます。

A. 複数回起こるイベントの一連の処理をコードに書く場合
  1. Timerを使ってグラデーションする点滅表示
  2. マウスやキーのDown/Upイベントでドラッグや押し続けの処理
  3. jQueryのコールバック関数でイベントを待つ処理
B. 非同期で処理して相手側の終了を待つコードを書く場合
  1. WebClientでダウンロードの完了を非同期で待機する処理
  2. 名前付きパイプでデータの受信を非同期で待機する処理

以降で、これらの例をこの順で説明していきます。

■wait文を使いたくなる非同期コードの例 : Timerでグラデーション表示/マウスやキーのDown-Upイベントでドラッグや押し続けの処理/WebClientでDL待機/jQueryのコールバック

A.1. Timerを使ってグラデーションする点滅表示
~ 反復するイベント処理で威力を発揮する ~

何度も繰り返し発生するイベント処理の中で構文「wait イベント名」を使えば、コードがかなり書きやすくなります。はじめに、構文「wait イベント名」のない現在のC#で、2色の間をグラデーションしながら点滅する例を考えます。よく点滅の動作はTimerが使われます。ここでもTimerを使って、例えば下のコードのように実装します。

    private int _Bright = 0; // ←[A]
    private int _Step = 10; // ←[B]
    private void timer1_Tick(object sender, EventArgs e) { // ←[C]

        _Bright += _Step; // ←[D]
        if (_Bright > 255) { // ←[E]
            _Step = -10;
            _Bright += _Step;
        } else if (_Bright < 0) {
            _Step = 10;
            _Bright += _Step;
        }

        label1.BackColor = Color.FromArgb( // ←[F]
                               _Bright, _Bright, _Bright);
    }
Timerを使って2色の間をグラデーションしながら点滅させる

[A]の_Brightは0~255の間で今の色の明るさが入っていて、最後に[F]で_Brightから色を作ります。[B]の_Stepは+10か-10の値で明るさの増減量が入っています。_Stepが+10なら_Brightは10ずつ増えていき、-10なら10ずつ減っていきます。[C]の関数がTimerのTickイベントを受け、一定間隔で繰り返し呼ばれます。[D]で_Brightを増減させます。[E]のif...else if...文で_Stepの値を変え、増減の方向を反対にします。[F]で_Brightから色を作り、ラベルの背景色にセットします。

これによって、ラベルの背景色が黒→白→黒→白...とグラデーションしながら点滅します。Timerを使ってアニメーションしようとすると、よくこのようなコードになる傾向があります。

構文「wait イベント名」を使う

次に、構文「wait イベント名」を使って同じ動作を書くと、例えば下のコードのようになります。構文「wait イベント名」を使うと、コードの見た目が劇的に変わります。

    private void Form1_Load(object sender, EventArgs e) { // ←[A]
				  
        while (true) { // ←[B]

            for (var bright = 0; bright < 255; bright += 10) { // ←[C]
                label1.BackColor = Color.FromArgb( // ←[D]
                                       bright, bright, bright);
                wait timer1.Tick; // ←[E]
            }

            for (var bright = 250; bright > 0; bright -= 10) { // ←[F]
                label1.BackColor = Color.FromArgb( // ←[G]
                                       bright, bright, bright);
                wait timer1.Tick; // ←[H]
            }
        }
    }
構文「wait イベント名」を使って2色の間をグラデーションしながら点滅させる

コードは[A]のようにFormのLoadイベントに書きます。Formがロードされると、[A]が呼ばれて[B]以降のコードが実行されます。[B]のwhile文で無限ループを作り、何度も同じ動作を繰り返します。

無限ループの中には、[C]と[F]の2つのfor文があります。[C]ではbrightを0から順に10ずつ上げて、250になるまで繰り返します。[D]でbrightから色を作り、ラベルの背景色にセットして、[E]でwait文を使ってTimerのTickイベントが発生するのを待ちます。これによって、Tickイベントが発生する度にfor文が回り、brightが10ずつ上がって背景色が明るくなっていきます。[F]も[C]と同じようにしてbrightを10ずつ下げて、0になるまで繰り返し、背景色を暗くしていきます。

[F]のfor文が終了すると、[B]の無限ループにより、再度[C]から実行して、ずっと点滅が続きます。[E]や[H]のwait文があるので、[B]の無限ループがCPUを100%にすることはありません。

構文「wait イベント名」を使わない場合と使った場合のコードを比べると、コードの量は約13行と同じぐらいになりますが、コードの流れはwait文を使った方が分かりやすくなっています。そのため、コードが直感的に読みやすくなります。wait文を使わない場合は、_Stepのような増減量の考えを持ち出して工夫しなくてはならず、コードを直感的に書けません。一方、wait文を使うと、このような凝ったテクニックなしで、やりたいことをそのまま書くことが出来ます。

このように、Timerのような何度も繰り返し発生するイベント処理の中で構文「wait イベント名」を使えば、コードの読みやすさや書きやすさが飛躍的に向上します。

A.2. マウスやキーのDown/Upイベントでドラッグや押し続けの処理
~ 流れがあるイベント処理で威力を発揮する ~

処理に順番があるような流れのあるイベント処理の中で構文「wait イベント名」を使えば、コードがかなり読みやすくなります。はじめに、構文「wait イベント名」のない現在のC#で、マウスをドラッグして矩形範囲を指定する例を考えます。下の図のようなイメージで、マウスを押しながら動かして放すと、押してから放したまでの矩形範囲がラベルに表示されます。

マウスをドラッグして矩形範囲を指定する例

この例を3つのマウスイベントMouseDown、MouseMove、MouseUpを使って、例えば下のコードのように実装します。

bool _IsSelecting = false; // ←[A]
Point _PosStart; // ←[B]

private void Form1_MouseDown(object sender, MouseEventArgs e) { // ←[C]

    _IsSelecting = true; // ←[D]
    _PosStart = e.Location; // ←[E]
    label1.Text = string.Format("From {0} To ---", _PosStart); // ←[F]
}

private void Form1_MouseMove(object sender, MouseEventArgs e) { // ←[G]

    if (_IsSelecting) { // ←[H]
        label1.Text = string.Format("From {0} To {1}", _PosStart, e.Location); // ←[I]
    }
}

private void Form1_MouseUp(object sender, MouseEventArgs e) { // ←[J]

    _IsSelecting = false; // ←[K]
    label1.Text = string.Format("From {0} To {1}", _PosStart, e.Location); // ←[L]
}
マウスをドラッグして矩形範囲を指定する

[A]の_IsSelectingは今矩形範囲を選択中かどうかの状態が入っています。[B]の_PosStartは矩形範囲の選択開始位置(X,Y座標値)が入ります。まず、マウスを押すと[C]が呼ばれて、[D]で状態を選択中にし、[E]で選択開始位置を保存します。[F]で今の矩形範囲を表示します。次に、マウスを動かすと[G]が呼ばれます。クリックしながらマウスを動かすと、[H]の_IsSelectingがtrueになるので、[I]で今の矩形範囲がその都度更新されます。最後に、マウスを放すと[J]が呼ばれて、[K]で状態を元に戻し、[L]で最終的な矩形範囲を表示します。

これによって、マウスをドラッグすると矩形範囲が選択されます。マウスやキーのDown/Upイベントを使って動作を拡張しようとすると、よくこのようなコードになる傾向があります。

構文「wait イベント名」を使う

次に、構文「wait イベント名」を使って同じ動作を書くと、例えば下のコードのようになります。構文「wait イベント名」を使うと、3つの関数が1つになってコードの見た目が劇的に変わります。

private void Form1_MouseDown(object sender, MouseEventArgs e) { // ←[A]

    var pos_start = e.Location; // ←[B]
    label1.Text = string.Format("From {0} To ---", pos_start); // ←[C]

    while (true) { // ←[D]

        wait this.MouseMove or this.MouseUp; // ←[E]
        if (this.MouseUp.IsRaised) // ←[F]
            break; // ←[G]
        
        label1.Text = string.Format("From {0} To {1}",  // ←[H]
                          pos_start, this.MouseMove.Args.e.Location);
    }

    label1.Text = string.Format("From {0} To {1}",  // ←[I]
                      pos_start, this.MouseUp.Args.e.Location);
}
構文「wait イベント名」を使って矩形範囲を指定する

コードは[A]のようにFormのMouseDownイベントに書きます。Formの上でマウスが押されると、[A]が呼ばれて[B]以降のコードが実行されます。 マウスを押すと、まず[B]のe.Locationでマウスのポインタ位置を取得して、矩形範囲の選択開始位置をpos_startに入れます。[C]で今の矩形範囲を表示します。

[D]のwhile文でループを作り、イベントを複数回待つようにします。[E]でwait文を使ってMouseMoveイベントとMouseUpイベントが発生するのを待ちます。wait文の中で2つのイベントがorで並んでいます。これは、どちらか片方のイベントが発生するまでその行で実行が待機されます。[F]でこれら2つのイベントのうち、どちらが発生したかを調べます。ここでC#の書き方を拡張して、イベントに.IsRaisedを付けると、直前でそのイベントが発生したかどうかを調べられるようにします。これによって、[F]ではMouseUpイベントが発生した場合、IsRaisedがtrueを返し[G]のbreakに来て、while文のループを抜けます。

一方、[E]でMouseMoveイベントが発生すると[H]へ進み、今の矩形範囲を更新します。ここでマウスのポインタ位置を取得するためにC#を拡張して、イベントに.Argsを付けると、そのイベント引数にアクセスできるようにします。MouseMoveイベントの引数はsenderとeなので、this.MouseMove.Args.eと書いてeの中にアクセスできます。[H]で矩形範囲を更新すると、[D]のループにより再度[E]に戻り、MouseMoveイベントとMouseUpイベントを待ちます。

[E]でMouseUpイベントが発生してループを抜けると、[I]で最終的な矩形範囲を表示します。ここではthis.MouseUpの方のArgsからeにアクセスして、マウスを放した際のポインタ位置を取得します。

C#の書き方を拡張する

前述のコードでは、構文「wait イベント名」を効果的に使うために、いろいろとC#の書き方を拡張しました。ここで、拡張した内容を次の表に整理します。

1. wait文の中でorと書ける
  「wait イベント1 or イベント2」と書いて、イベント1とイベント2のどちらか片方が発生するまでその行で実行を一時停止して待機するようにwait文を拡張します。同じようにAND結合も考えられます。
2. イベント.IsRaisedと書ける
  イベントに.IsRaisedを付けると、直前でそのイベントが発生したかどうかを調べられるようにC#を拡張します。イベント.IsRaisedがtrueを返せば、そのイベントが発生したことが分かります。
3. イベント.Argsと書ける
  イベントに.Argsを付けると、そのイベントの引数にアクセスできるようにC#を拡張します。.NETのWindows Formのイベントは、引数が常にsenderとeなので、イベント.Args.eと書いてeの中身にアクセスできます。

構文「wait イベント名」を使わない場合と使った場合のコードを比べると、コードの量は大差ありませんが、コードの流れはwait文を使った方が分かりやすくなっています。wait文を使わない場合のコードを見ると、Form1_MouseDown/Move/Upの3つのイベントハンドラにある関係が、ぱっと見てよく分かりません。上から順にコードを読んでも、どのイベントがどのように発生して、全体としてどのように実行されるのかが分かりません。コードを理解するには、これらの謎を紐解かないといけません。一方、wait文を使うと、イベントが発生する順番どおりにコードが並びます。コードを理解するには、上から順にコードを辿るだけでよく、コードが直感的に理解しやすくなります。

wait文を使わない場合、Form1_MouseDown/Move/Upの3つのイベントハンドラを別々の関数として書きます。また、これらの関数の間をつなぐために、関数の外に2つの変数_IsSelecting、_PosStartを宣言します。このように関連したコードが3つの関数と2つの変数に分散してしまいます。一方、wait文を使うと、Form1_MouseDownの1つにすべてのコードが入ります。関連したコードが1つの関数にまとまりコードの管理が容易になります。

このように、マウスやキーイベントのようなイベントの発生順序に流れのあるイベント処理の中で構文「wait イベント名」を使えば、コードの読みやすさや管理のしやすさが飛躍的に向上します。

■C#の拡張が多い→再度、表でまとめる
■利点 : 3つのイベントを3関数で→1つにまとめられる、外に出る2変数が関数の中に入る、1つの動作を変数/関数共に1つにまとめられる : イベントの流れのままをコードに書ける

A.3. jQueryのコールバック関数でイベントを待つ処理
~ jQueryの中で威力を発揮する ~

jQueryの中で構文「wait イベント名」を使えば、コードがかなりすっきり書けるようになります。構文「wait イベント名」のない現在のjQueryではイベントを処理するためにコールバック関数を指定します。例えば下のコードでは、HTMLのDiv要素を1秒かけて表示して(フェードイン)、その後1秒かけて消します(フェードアウト)。

    $("Div").fadeIn(1000, function () { // 匿名関数A
        $("Div").fadeOut(1000, function () { // 匿名関数B
            alert("終了");
        });
    });
jQueryでフェードイン/アウトのイベントを処理する

この中でfunction () {...}の形をした匿名関数の部分がコールバック関数になります。1秒かけてフェードインが終了すると、fadeIn関数に指定した匿名関数Aが実行されます。匿名関数Aの中にfadeOut関数があるので、今度は1秒かけてフェードアウトします。フェードアウトが終了すると、fadeOut関数に指定した匿名関数Bが実行されます。匿名関数Bの中にalert関数があるので、最後に「終了」とダイアログが表示されます。

このフェードイン/アウトの例では、順番に2つの終了イベントを待つだけですが、if文でイベントを選んで待機したり、for文でイベントを繰り返し待機したりと、もっと複雑に流れを制御しようと思うと、このようなコールバック関数を使ってイベントを処理する書き方では、コードが必要以上に複雑になり、流れが読み取りにくくなります。

構文「wait イベント名」を使う

jQueryの中で構文「wait イベント名」を使えば、複雑に流れを制御する場合でもコードがすっきり書けるようになります。コードに流れが残るので、直感的に理解しやすいコードになります。フェードイン/アウトの例を構文「wait イベント名」を使って書き直すと、例えば次のようになります。

    wait $("Div").fadeIn(1000).event("finish"); // ←[A]
    wait $("Div").fadeOut(1000).event("finish"); // ←[B]
    alert("終了"); // ←[C]
フェードイン/アウトのイベントを構文「wait イベント名」を使って待機する

このコードは書き直す前のコードと同じ動作をします。[A]の行で1秒かけてフェードインし、wait文でその終了を待ちます。フェードインが終了すると[B]の行に進んで、1秒かけてフェードアウトし、wait文でその終了を待ちます。フェードアウトが終了すると[C]の行に進んで、「終了」とダイアログが表示されます。

構文「wait イベント名」を使うにはイベントが必要なので、例えばjQueryにevent関数を新しく追加します。そしてevent("finish")と書くことで、直前にあるfadeInやfadeOutが終了したときのイベントを取得できるようにjQueryを拡張します。これらの終了イベントを待つためにwait文を「wait ***.event("...");」の形で使用します。

構文「wait イベント名」を使わない場合と使った場合のコードを比べると、コードがシンプルになっています。匿名関数のネストがなくなり、コードの流れがよく分かります。waitというキーワードから、その行で実行が待機されることがすぐに理解できます。event("finish")関数の名前から、その行では終了のイベントを待機することが簡単に予測できます。このようにコードを順に読むだけで、何をするのかがイメージできます。

もっと複雑に流れを制御する

構文「wait イベント名」でコードがシンプルになったので、より複雑なことも簡単に書けるようになります。そこで、構文「wait イベント名」を使って、もっと複雑に流れを制御する場合を考えます。例えば、下の図のようにWebページにあるサムネイルの絵を拡大表示させる例を考えます。サムネイルをクリックするとロード中の表示が出て、バックグラウンドで拡大絵をロードします。ロードが完了するとロード中の表示が消えて、拡大絵が前面に表示されます。

Webページにあるサムネイルの絵を拡大表示 [拡大]

ここで、拡大表示するプログラムを作成するために、動作の流れを考えます。すべての動作パターンを考えて、一連の処理の流れを下のアクションフロー図にまとめます。すると、この拡大表示する動作には、複数のイベントを決まった順序で待機したり、イベントの種類によって動作を分岐させたりと、複雑に流れを制御しなくてはならないことが分かります。

サムネイルを拡大表示するアクションフロー図

動作はまず、[A]サムネイルをクリックするイベントが発生すると、[B]ロード中の表示を出して、[C]拡大絵をロードします。次に、[D]2つのイベントを待ちます。拡大絵のロードが完了するイベントと画面のどこかをクリックするイベントを待ちます。もしロードが完了すれば、[E]ロード中の表示を消して、[G]拡大絵を前面に表示します。もしロード中に画面のどこかをクリックすれば、ロードをキャンセルします。[E]ロード中の表示を消して、[F]動作を終了します。一方、拡大絵を表示した後は、[H]画面のどこかをクリックするイベントを待ちます。イベントが発生すると、[I]拡大絵を消して、動作を終了します。

動作の流れが明確になったところで、上のアクションフロー図を基に、構文「wait イベント名」を使ってコードを書いてみます。コードは、例えば次のようになります。アクションフロー図とコードの中にある[A]から[I]までの記号は、それぞれの対応する項目です。ここでアクションフロー図とコードを見比べると、アクションフロー図の各項目がそのままコードになったような形になります。コードの流れが分かりやすいのは、このためです。

$("Img").click(function () { // ←[A]

    $("Div#loading").show(); // ←[B]
    $("Div#zoomed Img").attr("scr", "***.png"); // ←[C]

    wait $("Div#zoomed").event("loaded") or 
                    var e1 = $("Body").event("click"); // ←[D]
    $("Div#loading").hide(); // ←[E]
    if (e1.isRaised) // ←[F]
        return;

    $("Div#zoomed").show(); // ←[G]
    wait $("Body").event("click"); // ←[H]
    $("Div#zoomed").hide(); // ←[I]
});
Webページにあるサムネイルの絵を拡大表示させるコード

このように、構文「wait イベント名」を使えば、複雑に流れを制御するコードも簡単に書けるようになります。開発者が思い浮かべる動作の流れどおりにコードを書けるので、コードが書きやすくなります。また、動作の流れがコードにそのまま残るので、コードが直感的に読みやすくなります。

構文「wait イベント名」を使わずに、今のjQueryだけで拡大表示する例のコードを書くと、どんなコードになるか想像してみて下さい。もっと複雑で入り組んだコードになります。構文「wait イベント名」の効果の大きさが分かります。

引き付け文→Beforeコード抜粋→Afterコード→良くなった点→発展系
wait文を使わずに今のjQueryだけで拡大表示するコードを書くとどんな感じになるか想像してみる...もっと複雑になる、wait文の効果の大きさが分かる

B.1. WebClientでダウンロードの完了を非同期で待機する処理
~ 非同期イベントの待機で威力を発揮する ~

非同期イベントを待機する処理の中で構文「wait イベント名」を使えば、コードの流れが理解しやすくなります。はじめに、構文「wait イベント名」のない現在のC#で、Webからあるファイルをダウンロードする例を考えます。ダウンロードの動作はWebClientを使って、ダウンロードの完了をイベントで待機します。例えば下のコードのように実装します。

    private void button1_Click(object sender, EventArgs e) { // ←[A]

        button1.Enabled = false; // ←[B]

        var webc = new System.Net.WebClient(); // ←[C]
        webc.DownloadFileCompleted += (_sender, _e) => { // ←[D]

            Console.WriteLine("ダウンロードしました。"); // ←[E]
            button1.Enabled = true; // ←[F]
        };
        webc.DownloadFileAsync( // ←[G]
            new Uri("http://www.google.co.jp/"), "downloaded.html");
    }
WebClientを使ってWebからファイルをダウンロードする

上のコードはボタンbutton1をクリックすると、Googleのトップページがダウンロードされます。処理は[A]のようにbutton1のClickイベントハンドラに書きます。[B]でダウンロード中はボタンを無効にします。[C]でWebClientクラスをnewして、ダウンロードの機能を作成します。

ダウンロードが完了したときに処理が出来るようにするため、[D]でDownloadFileCompletedイベントにラムダ式を登録します。これによって、ダウンロードが完了すると、[E]と[F]が実行されます。[E]でコンソールに完了を表示し、[B]で無効にしたボタンを[F]で元に戻します。[G]でWebClientのDownloadFileAsyncを呼び出して、ダウンロードを開始します。Asyncの付いたこの関数を使うことで、ダウンロードが別スレッドで並行して(=非同期で)進みます。

これによって、ボタンbutton1をクリックすると、Googleのトップページが非同期でダウンロードされます。このように非同期の処理をしようとすると、よく完了イベントを使ったコードになる傾向があります。

構文「wait イベント名」を使う

次に、構文「wait イベント名」を使って同じ動作を書くと、例えば下のコードのようになります。構文「wait イベント名」を使うと、ラムダ式がなくなってコードの見た目が変わります。

    private void button1_Click(object sender, EventArgs e) { // ←[A]

        button1.Enabled = false; // ←[B]

        var webc = new System.Net.WebClient(); // ←[C]

        var event_completed = webc.DownloadFileCompleted; // ←[D]
        webc.DownloadFileAsync( // ←[E]
            new Uri("http://www.google.co.jp/"), "downloaded.html");

        wait event_completed; // ←[F]

        Console.WriteLine("ダウンロードしました。"); // ←[G]
        button1.Enabled = true; // ←[H]
    }
構文「wait イベント名」を使ってWebからファイルをダウンロードする

コードは[A]から[C]までは同じです。[D]でDownloadFileCompletedイベントを変数event_completedに代入して保存します。これは、いつイベントが発生しても漏れなく検知するために、[E]のDownloadFileAsyncを呼び出す前にイベントを取得しておく必要があるためです。そして、[E]でDownloadFileAsyncを呼び出して、ダウンロードを開始します。Asyncの付いたこの関数を使って、ダウンロードを別スレッドで並行して行います。

[F]でダウンロードが完了するまで待機します。wait文を使ってDownloadFileCompletedイベントが発生するのを待ちます。[G]でコンソールに完了を表示し、[B]で無効にしたボタンを[H]で元に戻します。

構文「wait イベント名」を使わない場合と使った場合のコードを比べると、コードの量は約10行と同じぐらいになりますが、コードの流れはwait文を使った方が分かりやすくなっています。wait文を使わない場合は、ラムダ式を使うので、コードの実行順序が書いてある順番どおりではなくなり、ラムダ式の中のコードが実際には後で実行されます。そのため、コードの流れが直感的に分かりにくくなります。一方、wait文を使うと、上から下へ書いてある順番どおりに実行され、コードの流れをそのまま理解しやすくなります。

このように、DownloadFileCompletedのような非同期イベントを待機する処理の中で構文「wait イベント名」を使えば、コードの流れが読み取りやすくなります。

B.2. 3つのスレッドを同期させる処理
~ 別スレッドの待機に適用する ~

別のスレッドを待機する処理の中でも構文「wait イベント名」を使うことが出来ます。はじめに、構文「wait イベント名」のない現在のC#で、別々に実行している3つのスレッドを同期させる例を考えます。ここでは、同期させるためにManualResetEventクラスを使って、例えば下のコードのように実装します。

private void button1_Click(object sender, EventArgs e) { // ←[A]

    var events = new[] { // ←[B]
        new ManualResetEvent(false), 
        new ManualResetEvent(false), 
        new ManualResetEvent(false)
    };
    ParameterizedThreadStart content = (id_obj) => { // ←[C]

        var id = (int)id_obj; // ←[D]
        Console.WriteLine("[{0}] Started.", id);

        Thread.Sleep(id * 100); // ←[E]
        Console.WriteLine("[{0}] Waited for {1} msec.", id, id * 100);

        events[id].Set(); // ←[F]
        WaitHandle.WaitAll(events); // ←[G]
        Console.WriteLine("[{0}] Finished.", id);
    };

    new Thread(content).Start(0); // ←[H]
    new Thread(content).Start(1);
    new Thread(content).Start(2);
}
別々に実行している3つのスレッドを同期させる

ボタンbutton1をクリックすると処理が始まるように、コードを[A]のClickイベントハンドラに書きます。[B]で3つのスレッドで使うManualResetEventクラスをnewします。ManualResetEventは内部にtrueかfalseの状態を持ち、状態がtrueになるまでスレッドを待機させることが出来ます。[C]でラムダ式を作り、スレッドで実行する中身を実装します。ラムダ式には引数id_objを持たせて、それぞれのスレッドに対して別々の番号を割り振ることで、スレッドを見分けます。

スレッドの中身は[D]から[G]まであり、[D]でスレッド別に割り振った番号をint型に直します。引数id_objはobject型しか許されないので、[D]でint型にキャストします。[E]でスレッドを数百ミリ秒の間待機させます。[F]で自分のスレッドに割り当てられたManualResetEventクラスを参照してSet()を呼び出し、内部の状態をtrueにします。これによって、スレッドの実行が[F]まで到達すると自分の状態がtrueに変わります。

[G]で他のすべての状態がtrueになるまで自分のスレッドを待機させます。待機にはWaitHandleクラスのWaitAllを使います。これによって、3つのスレッドを同期します。[C]から[G]でスレッドの中身を記述した後、[H]で3つのスレッドを作成し実行します。スレッド別に0、1、2の番号を割り振って、Startの引数に入れます。

上のコードはボタンbutton1をクリックすると、3つのスレッドが実行されて、コンソールには次のような結果が表示されます。表示の順番は毎回の試行で変わりますが、[G]でスレッド間の同期を取るので、「Finished.」の表示は必ずどの「Waited for...」よりも後に表示されます。

[0] Started.
[1] Started.
[0] Waited for 0 msec.
[2] Started.
[1] Waited for 100 msec.
[2] Waited for 200 msec.
[1] Finished.
[2] Finished.
[0] Finished.
上のコードを実行した場合のコンソールに出力される結果

これによって、別々に実行している3つのスレッドが同期します。スレッドの間で同期を取ろうとすると、よくこのようなWaitHandleクラスを使ったコードになる傾向があります。

構文「wait イベント名」を使う

次に、構文「wait イベント名」を使って同じ動作を書くと、例えば下のコードのようになります。コードは[G]の行だけ変わります。

    private void button1_Click(object sender, EventArgs e) { // ←[A]

        var events = new[] { // ←[B]
            new ManualResetEvent(false), 
            new ManualResetEvent(false), 
            new ManualResetEvent(false)
        };
        ParameterizedThreadStart content = (id_obj) => { // ←[C]

            var id = (int)id_obj; // ←[D]
            Console.WriteLine("[{0}] Started.", id);

            Thread.Sleep(id * 100); // ←[E]
            Console.WriteLine("[{0}] Waited for {1} msec.", id, id * 100);

            events[id].Set(); // ←[F]
            wait events[0] and events[1] and events[2]; // ←[G]
            Console.WriteLine("[{0}] Finished.", id);
        };

        new Thread(content).Start(0); // ←[H]
        new Thread(content).Start(1);
        new Thread(content).Start(2);
    }
構文「wait イベント名」を使って別々に実行している3つのスレッドを同期させる

ほとんどのコードは同じままで、[G]の行だけwait文を使ったコードに変わります。[G]ですべての状態がtrueになるのを待つために、eventsの全項目をandでつなぎます。このように、構文「wait イベント名」はWaitHandle.WaitAllと同じ役割を果たすことが分かります。

一方、WaitHandle.WaitAllはスレッドの待機にしか使えませんが、構文「wait イベント名」はイベントやスレッドなど、同期/非同期関係なく何でも待機できます。また、and/orの結合を使って、待機の条件をきめ細かく指定できます。

このように、別のスレッドを待機する処理の中でも、構文「wait イベント名」を同じように使うことが出来ます。

複数ある待機方法を統一する

構文「wait イベント名」を使わない場合、前述のTimer点滅表示の例(A.1)と、ダウンロード待機の例(B.1)、パイプの受信待機の例(B.2)の3つは、どれもある時間の間待つ動作をするコードですが、それぞれのコードの書き方は異なります。これら3つの例に対するコードは、待つ動作部分を抜粋して挙げると、次のようになります。

private int _Bright = 0;
private void timer1_Tick(object sender, EventArgs e) {
    ...中略...
    label1.BackColor = Color.FromArgb(_Bright, _Bright, _Bright);
}
A.1. Timer点滅表示の例を実現するコードの抜粋
var webc = new System.Net.WebClient();
webc.DownloadFileCompleted += (_sender, _e) => {
    Console.WriteLine("ダウンロードしました。");
};
webc.DownloadFileAsync(new Uri("http://www.google.co.jp/"), "downloaded.html");
B.1. ダウンロード待機の例を実現するコードの抜粋
    ...中略...
    events[id].Set();
    WaitHandle.WaitAll(events);
    ...中略...
B.2. 3つのスレッドを同期させる例を実現するコードの抜粋

3つのコードを眺めると、どれも動作はある時間待機するコードですが、次のような違いがあります。

  • A.1とB.1の例はイベントの機能を使って待機するのに対して、B.2の例はWaitHandleの関数を使って待機します。
  • A.1は同じスレッドの中で発生するイベントを待機するのに対して(同期処理)、B.1とB.2は別のスレッドで発生するイベントや状態変化を待機します(非同期処理)。

待つという動作に注目すると、今の言語にはこのように複数の待機方法があり、1つに統一してシンプルにした方が分かりやすくなります。そこで、構文「wait イベント名」を導入して、待つという動作は一律wait文で書くようにします。これによって、wait文でイベントもスレッドも同じように待機できるので、イベントとスレッドをand/or結合して、次のコードのように待つことも可能になります。

    var webc = new System.Net.WebClient();
    events[id].Set();
    wait webc.DownloadFileCompleted and events[0];
イベントとスレッドをand/or結合して待機する

1.Timerグラデーション表示、2.非同期DLの待機、3.スレッドのJoin、の3つを実現するコードは、それぞれ性質が異なる→1は同期処理で反復イベントの発生を待機、2は非同期処理でイベントの発生を待機、3は非同期処理でThreadのJoinで待機→同期/非同期=1スレッド/複数スレッド、イベント/Join→待機の動作に注目すると、今の言語には複数の待機方法があり1つに統一してシンプルにできる→wait文の導入で実現
wait文でイベントもスレッドも統一して待機できるので、イベントとスレッドをAND/ORで待つことも可能→wait event1 and/or thread1.join;

yield文を使ってwait文の機能を実現する

本記事で提案する構文「wait イベント名」はアイディアの1つで、まだ実現されていません。しかし、yield文が使えるプログラム言語ではwait文の機能を擬似的に実現することが出来ます。yield文を使ったwait文の模擬方法については、別ページ「yield文でイベントを待機する」に記載しました。

世の中の似た機能

多くのプログラム言語にはスレッドを待機させる機能があります。C#では、例えばWaitHandle.WaitAll(...)を使って、ある条件を満たすまでスレッドの実行を停止させて、コードの実行をその行で止めることが出来ます。一方、本記事で提案する構文「wait イベント名」はスレッドと同じ感覚でイベントも待機できるようにしたものです。

Verilog言語の文法には@キーワードがあります。この@(変数名)を使うと、指定した変数が条件を満たすまでその行でコードの実行が待機されます。VerilogはFPGAなどICチップのデジタル回路を設計する言語で、その用途や動作方法がCPUとまったく違いますが、プログラムの考え方として共通する部分があります。

■用途、実装方法が違うがプログラムの考え方としてVerilogシミュモードに@の書き方がある

 

更新履歴
2010/02/26
  • 書き始め
2010/04/26 v1
  • 初版作成

※ご意見、ご感想、改善点、その他の情報などがありましたら、メールにてお知らせ願います。

Copyright (C) 2002 - 2019 Simon.P.G. All Rights Reserved. Top | Simon.P.G. とは | 使用条件 | ご意見
inserted by FC2 system