コードの非同期バージョンと同期バージョンの両方が必要な場合に、DRY原則に違反しないようにするにはどうすればよいですか?


15

同じロジック/メソッドの非同期バージョンと同期バージョンの両方をサポートする必要があるプロジェクトに取り組んでいます。だから例えば私は持っている必要があります:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

これらのメソッドの非同期ロジックと同期ロジックは、1つは非同期でもう1つはそうでないことを除いて、すべての点で同じです。このようなシナリオでDRYの原則に違反しないようにする正当な方法はありますか?GetAwaiter()。GetResult()を非同期メソッドで使用して、それを同期メソッドから呼び出すことができると人々が言っ​​ているのを見ましたか?そのスレッドはすべてのシナリオで安全ですか?これを行う別のより良い方法はありますか、それともロジックを複製することを余儀なくされていますか?


1
ここであなたがしていることについてもう少し話していただけますか?具体的には、非同期メソッドで非同期をどのように実現していますか?高レイテンシの作業CPUバウンドまたはIOバウンドですか?
Eric Lippert

「1つは非同期でもう1つは非同期ではない」とは、実際のメソッドのコードまたは単なるインターフェースの違いですか。(明らかにインターフェースだけで可能ですreturn Task.FromResult(IsIt());
Alexei Levenkov

また、おそらく同期バージョンと非同期バージョンはどちらも高遅延です。呼び出し側はどのような状況で同期バージョンを使用し、呼び出し側は呼び出し先が数十億ナノ秒かかる可能性があるという事実をどのように緩和するのでしょうか?同期バージョンの呼び出し元は、UIがハングしていることを気にしませんか?UIはありませんか?シナリオについて詳しく教えてください。
Eric Lippert

@EricLippert私は、2つのコードベースが非同期であるという事実以外に、2つのコードベースが同じであるという感覚を与えるために、特定のダミーの例を追加しました。この方法がはるかに複雑で、コードの行と行を複製する必要があるシナリオを簡単に想像できます。
マルコ

@AlexeiLevenkov違いは確かにコードにあり、私はそれをデモするためにいくつかのダミーコードを追加しました。
マルコ

回答:


15

あなたはあなたの質問にいくつかの質問をしました。私はあなたとは少し違う方法でそれらを分解します。しかし、最初に直接質問に答えさせてください。

私たちは皆、軽量で高品質、そして安価なカメラを求めていますが、言われているように、これらの3つのうち多くても2つしか入手できません。ここでも同じ状況です。効率的で安全で、同期パスと非同期パスの間でコードを共有するソリューションが必要です。あなたはそれらのうちの2つだけを手に入れるでしょう。

それがなぜなのかを詳しく見てみましょう。私たちはこの質問から始めましょう:


私は人々があなたがGetAwaiter().GetResult()非同期メソッドで使用し、それをあなたの同期メソッドから呼び出すことができると言うのを見ましたか?そのスレッドはすべてのシナリオで安全ですか?

この質問の要点は、「同期パスを非同期バージョンで同期待機させるだけで、同期パスと非同期パスを共有できるか」ということです。

重要なので、この点についてははっきりさせておきます。

あなたはそれらの人々からのアドバイスをとるのを直ちに止めるべきです。

それは非常に悪いアドバイスです。非同期タスクから結果を同期的にフェッチすることは非常に危険ですタスクが正常または異常に完了したという証拠がない限りです

これが非常に悪いアドバイスである理由は、まあ、このシナリオを検討するためです。芝生を刈り取りたいが、芝刈り機のブレードが壊れている。次のワークフローに従うことにしました。

  • Webサイトから新しいブレードを注文してください。これは、待機時間の長い非同期操作です。
  • 同期的に待機します。つまり、ブレードが手に入るまでスリープします。。。
  • メールボックスを定期的にチェックして、ブレードが届いたかどうかを確認してください。
  • ブレードを箱から取り出します。今あなたはそれを手にしています。
  • ブレードを芝刈り機に取り付けます。
  • 芝を刈る。

何が起こるのですか?操作ので、あなたは永遠に眠るメールをチェックがされるようになりましたメールが到着した後に起こる何かにゲートを

同期的に任意のタスクを待機する場合、この状況に陥るのは非常に簡単です。そのタスクは、現在待機しているスレッドの将来にスケジュールされた作業を持っている可能性がありますあり、今、あなたがそれを待っているので、その未来は決して到着しません。

非同期待機を行う場合、すべてが正常です。あなたは定期的にメールをチェックし、あなたが待っている間、あなたはサンドイッチを作るか、あなたの税金などをします。あなたはあなたが待っている間仕事をやり続けます。

同期的に待機することはありません。タスクが完了したら、それは不要です。タスクが完了していないが、現在のスレッドから実行されるようにスケジュールされている場合、現在のスレッドが待機する代わりに他の作業を処理している可能性があるため、非効率的です。タスクが完了しておらず、現在のスレッドでスケジュール実行が行われている場合、同期して待機するためにハングしています。タスクが完了したことがすでにわかっている場合を除いて、再び同期的に待機する正当な理由はありません。

このトピックの詳細については、以下を参照してください。

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

スティーブンは、現実のシナリオを私よりもはるかによく説明しています。


それでは、「別の方向」を考えてみましょう。非同期バージョンを単にワーカースレッドで同期バージョンにするだけでコードを共有できますか?

次の理由から、それはおそらく、実際にはおそらく悪い考えです。

  • 同期操作がレイテンシの高いIO作業の場合、非効率的です。これは本質的にワーカーを雇い、タスクが完了するまでワーカーをスリープさせます。スレッドはめちゃくちゃ高価です。デフォルトで最低100万バイトのアドレス空間を消費し、時間がかかり、オペレーティングシステムのリソースを消費します。無駄な作業をしているスレッドを焼きたくない。

  • 同期操作は、スレッドセーフになるように記述されていない場合があります。

  • これ、レイテンシの高い作業がプロセッサにバインドされている場合はより合理的な手法ですが、そうである場合は、単にワーカースレッドにハンドオフする必要はありません。タスク並列ライブラリを使用して、できるだけ多くのCPUに並列化したり、キャンセルロジックを使用したりする可能性があります。同期バージョンは、すべて非同期バージョンになっているため、単純にそれをすべて行うことはできません。

参考文献; 再び、スティーブンはそれを非常に明確に説明しています:

Task.Runを使用しない理由:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Task.Runのより多くの「すべきこと」と「すべきでないこと」のシナリオ:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


それでは何を残しますか?コードを共有するための両方の手法は、デッドロックまたは大きな非効率につながります。私たちが到達する結論は、あなたが選択をしなければならないということです。効率的で正しく、呼び出し元を喜ばせるプログラムが必要ですか、それとも、同期パスと非同期パスの間で少量のコードを複製することによって発生するいくつかのキーストロークを節約したいですか?あなたは両方を得ることはできません、私は恐れています。


Task.Run(() => SynchronousMethod())非同期バージョンで戻ることがOPが望んでいない理由を説明できますか?
ギヨームサディ

2
@GuillaumeSasdy:オリジナルのポスターは、作品が何であるかという質問に答えました:それはIOバウンドです。ワーカースレッドでIOバインドタスクを実行するのは無駄です。
Eric Lippert

わかりました。私はあなたが正しいと思います、そしてまた@StriplingWarriorの回答はそれをさらに説明します。
ギヨームサディ

2
@Markoスレッドをブロックするほとんどすべての回避策は、最終的に(高負荷下で)すべてのスレッドプールスレッド(通常は非同期操作を完了するために使用される)を消費します。その結果、すべてのスレッドが待機し、コードの「非同期操作が完了しました」部分を実行するために使用できるスレッドはありません。低負荷のシナリオではほとんどの回避策が適切です(各操作の一部としてブロックされているスレッドが2〜3個もあるため)…そして、非同期操作の完了が新しいOSスレッド(スレッドプールではない)で実行されることが保証できる場合すべてのケースでうまくいくかもしれません(あなたは高額を支払います)
Alexei Levenkov

2
「ワーカースレッドで単純に同期バージョンを実行する」以外に方法がない場合もあります。たとえばDns.GetHostEntryAsync、.NETでの実装方法とFileStream.ReadAsync、特定の種類のファイルについてです。OSは非同期インターフェースを提供しないだけなので、ランタイムはそれを偽造する必要があります(言語固有ではありません。たとえば、Erlangランタイムはワーカープロセスのツリー全体を実行し、それぞれに複数のスレッドを使用して、ノンブロッキングディスクI / Oと名前を提供します。解決)。
Joker_vD

6

これに万能の答えを出すのは難しいです。残念ながら、非同期コードと同期コードを再利用するための単純で完璧な方法はありません。しかし、考慮すべきいくつかの原則があります:

  1. 非同期コードと同期コードは基本的に異なることがよくあります。非同期コードには通常、たとえばキャンセルトークンを含める必要があります。そして多くの場合、異なるメソッドを呼び出す(例でQuery()は一方を呼び出すQueryAsync())か、異なる設定で接続をセットアップします。そのため、構造的に類似している場合でも、動作に十分な違いがあり、要件が異なる個別のコードとして扱われるメリットがあります。たとえば、FileクラスのメソッドのAsync実装とSync実装の違いに注意してください。同じコードを使用することはありません。
  2. インターフェースを実装するために非同期メソッドシグネチャを提供しているが、同期実装がある場合(つまり、メソッドの動作について本質的に非同期のものはない)、単純に返すことができます。 Task.FromResult(...)
  3. 2つのメソッド間で同じ同期ロジックの部分、別のヘルパーメソッドに抽出して、両方のメソッドで利用できます。

幸運を。


-2

簡単です。同期のものを非同期のものと呼びます。Task<T>それを行うための便利な方法もあります:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}

1
なぜこれが絶対にしてはいけない最悪の慣行なのか、私の答えを見てください。
Eric Lippert

1
あなたの答えはGetAwaiter()。GetResult()を扱います。私はそれを避けました。実際には、非同期にできない呼び出しスタックでメソッドを使用するために、メソッドを同期的に実行する必要がある場合があります。同期待機が行われないようにする必要がある場合は、C#チームの(以前の)メンバーとして、非同期タスクで1つではなく2つの方法で同期タスクをTPLに同期的に待機させる理由を教えてください。
キース

その質問をするのは私ではなく、スティーブン・トウブでしょう。私は物事の言語面でのみ作業しました。
Eric Lippert
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.