9

私の一般的な質問はこれです:同期ソリューションのように、まだ明確でわかりやすい非同期コードをどのように記述しますか?

私の経験では、BackgroundWorkerのようなものを使用して同期コードを非同期にする必要がある場合、全体的な意図とアクティビティの順序を表す一連の簡単なプログラムステートメントがなくなり、代わりに「 Done "イベントハンドラー。それぞれが次のBackgroundWorkerを開始し、追跡が非常に難しいコードを生成します。

私はそれがあまり明確ではないことを知っています。より具体的なもの:

WinFormsアプリケーションの関数が、いくつかのAmazon EC2インスタンスを起動し、それらが実行されるのを待ってから、それらがすべてSSH接続を受け入れるのを待つ必要があるとします。擬似コードの同期ソリューションは次のようになります。

instances StartNewInstances() {
    instances = StartInstances()
    WaitForInstancesToBecomeRunning(instances)
    WaitForInstancesToAcceptSSHConnection(instances).
    return (instances)
    }

それはすばらしい。何が起こっているかは非常に明確であり、プログラムアクションの順序は非常に明確です。コードとフローの理解を妨げるホワイトノイズはありません。本当にそのようなコードになりたいです。

しかし、実際には、同期ソリューションを使用することはできません。これらの各関数は長時間実行でき、それぞれが次のようなことを行う必要があります。UIを更新し、タイムアウトを超えているかどうかを監視し、次のようになるまで定期的に操作を再試行します。成功またはタイムアウト。つまり、フォアグラウンドUIスレッドを続行できるように、これらのそれぞれがバックグラウンドで発生する必要があります。

しかし、BackgroundWorkerのようなソリューションを使用する場合、上記のようなプログラムロジックを簡単に実行できるようにはならないようです。代わりに、UIスレッドからバックグラウンドワーカーを起動して最初の機能を実行し、ワーカースレッドの実行中にUIスレッドがUIに戻る場合があります。終了すると、その「完了」イベントハンドラーが次のバックグラウンドワーカーを開始する場合があります。終了すると、その「完了」イベントハンドラーが最後のBackgroundWorkerを開始する可能性があります。つまり、プログラムフロー全体を理解するには、DoneEventハンドラーの「トレイルをたどる」必要があります。

a)UIスレッドが応答するようにする、b)非同期操作でUIを更新できるようにする、そして最も重要なこととしてc)プログラムを一連の連続したステップとして表現できるようにする(これまでのように)より良い方法が必要です。上に示した)誰かが結果のコードを理解できるように

ありとあらゆる入力をいただければ幸いです。マイケル

4

5 に答える 5

11

私の一般的な質問は次のとおりです。同期ソリューションのように、明確で従うのが簡単な非同期コードをどのように記述しますか?

C# 5 を待ちます。そう遠くないでしょう。async/await岩。上記の文で機能について実際に説明しました...チュートリアル、言語仕様、ダウンロードなどについては、 Visual Studio async のホームページを参照してください。

現時点では、それほどクリーンな方法は実際にはありません。これが、そもそもこの機能が必要だった理由です。特にエラー処理などを考慮すると、非同期コードは非常に自然に混乱します。

コードは次のように表現されます。

async Task<List<Instance>> StartNewInstances() {
    List<Instance> instances = await StartInstancesAsync();
    await instances.ForEachAsync(x => await instance.WaitUntilRunningAsync());
    await instances.ForEachAsync(x => await instance.WaitToAcceptSSHConnectionAsync());
    return instances;
}

これは、フォームの拡張メソッドなど、少し余分な作業を想定していますIEnumerable<T>

public static Task ForEachAsync<T>(this IEnumerable<T> source,
                                   Func<T, Task> taskStarter)
{
    // Stuff. It's not terribly tricky :(
}
于 2012-07-26T17:15:08.883 に答える
3

Async/await本当に最善の方法です。ただし、待機したくない場合は、Continuation-passing-style、または CPS を試すことができます。これを行うには、処理が完了したときに呼び出される async メソッドにデリゲートを渡します。私の意見では、これは余分なイベントをすべて持つよりもクリーンです。

これにより、このメソッドのシグネチャが変更されます

Foo GetFoo(Bar bar)
{
    return new Foo(bar);
}

void GetFooAsync(Bar bar, Action<Foo> callback)
{
    Foo foo = new Foo(bar);
    callback(foo);
}

次に、それを使用するには、

Bar bar = new Bar();
GetFooAsync(bar, GetFooAsyncCallback);
....
public void GetFooAsyncCallback(Foo foo)
{
    //work with foo
}

GetFoo例外をスローする可能性がある場合、これは少し注意が必要です。私が好む方法は、 の署名を変更することですGetFooAsync

void GetFooAsync(Bar bar, Action<Func<Foo>> callback)
{
    Foo foo;
    try
    {
        foo = new Foo(bar);
    }
    catch(Exception ex)
    {
        callback(() => {throw ex;});
        return;
    }

    callback(() => foo);
}

コールバック メソッドは次のようになります

public void GetFooAsyncCallback(Func<Foo> getFoo)
{
    try
    {
        Foo foo = getFoo();
        //work with foo
    }
    catch(Exception ex)
    {
        //handle exception
    }
}

他の方法では、コールバックに 2 つのパラメーター (実際の結果と例外) を指定します。

void GetFooAsync(Bar bar, Action<Foo, Exception> callback);

これは、例外をチェックするコールバックに依存しているため、無視される可能性があります。他のメソッドには、成功用と失敗用の 2 つのコールバックがあります。

void GetFooAsync(Bar bar, Action<Foo> callback, Action<Exception> error);

私には、これによりフローがより複雑になり、例外を無視することができます。

ただし、結果を取得するためにコールする必要があるメソッドをコールバックに指定すると、コールバックは強制的に例外を処理します。

于 2012-07-26T17:35:01.150 に答える
3

Jon が正しく示唆しているように 5 を待てない場合は、Task Parallel Library (.NET 4 の一部) を参照することをお勧めします。質問で説明した「これを非同期で実行し、終了したらそれを実行する」パラダイムの周りに多くの配管を提供します。また、非同期タスク自体でのエラー処理もしっかりとサポートしています。

于 2012-07-26T17:20:05.183 に答える
1

終了すると、その「完了」イベントハンドラーが次のバックグラウンドワーカーを開始する場合があります。

これは私がしばらく苦労してきたことです。基本的に、UIをロックせずにプロセスが終了するのを待ちます。

ただし、backgroundWorkerを使用してbackgroundWorkerを開始する代わりに、1つのbackgroundWorkerですべてのタスクを実行できます。backgroundWorker.DoWork関数内では、そのスレッドで同期的に実行されます。したがって、3つのアイテムすべてを処理する1つのDoWork関数を持つことができます。

次に、1つのBackgroundWorker.Completedを待機し、「よりクリーンな」コードを取得する必要があります。

だからあなたは

BackgroundWorker_DoWork
  returnValue = LongFunction1
  returnValue2 = LongFunction2(returnValue)
  LongFunction3

BackgroundWorker_ProgressReported
  Common Update UI code for any of the 3 LongFunctions

BackgroundWorker_Completed
  Notify user long process is done
于 2012-07-26T17:25:33.950 に答える
0

一部のシナリオ (後で説明します) では、次の疑似コードのように非同期呼び出しをメソッドにラップできます。

byte[] ReadTheFile() {
  var buf = new byte[1000000];
  var signal = new AutoResetEvent(false);
  proxy.BeginReadAsync(..., data => {
    data.FillBuffer(buf);
    signal.Set();
  });
  signal.WaitOne();
  return buf;
}

上記のコードが機能するには、別のスレッドからコールバックを呼び出す必要があります。したがって、これは作業内容によって異なります。私の経験では、少なくとも Silverlight Web サービス呼び出しは UI スレッドで処理されます。つまり、上記のパターンは使用できません。UI スレッドがブロックされている場合、前の begin 呼び出しを実行することさえできません。この種のフレームワークを使用している場合、複数の非同期呼び出しを処理する別の方法は、より高いレベルのロジックをバックグラウンド スレッドに移動し、通信に UI スレッドを使用することです。ただし、このアプローチはほとんどの場合、バックグラウンド スレッドを開始および停止する定型コードが必要になるため、少しやりすぎです。

于 2012-07-26T18:12:26.727 に答える