web-dev-qa-db-ja.com

C#でのasync / awaitの使用に関するガイドラインは、優れたアーキテクチャと抽象化レイヤーの概念に矛盾していませんか?

この質問はC#言語に関するものですが、JavaまたはTypeScriptなど).

マイクロソフトでは、.NETでの非同期呼び出しの使用について ベストプラクティス を推奨しています。これらの推奨事項の中から、2つ選択しましょう。

  • 非同期メソッドの署名を変更して、TaskまたはTask <>を返すようにします(TypeScriptでは、Promise <>になります)。
  • xxxAsync()で終わるように非同期メソッドの名前を変更する

これで、低レベルの同期コンポーネントを非同期コンポーネントに置き換えると、アプリケーションのスタック全体に影響を与えます。 async/awaitは、「最後まで」使用された場合にのみプラスの影響を与えるため、アプリケーションのすべてのレイヤーのシグネチャとメソッド名を変更する必要があることを意味します。

優れたアーキテクチャでは、多くの場合、各レイヤーの間に抽象化を配置する必要があります。そのため、下位レベルのコンポーネントを他のコンポーネントで置き換えることは、上位レベルのコンポーネントには見えません。 C#では、抽象化はインターフェースの形式をとります。新しい低レベルの非同期コンポーネントを導入する場合、コールスタックの各インターフェイスを変更するか、新しいインターフェイスに置き換える必要があります。実装クラスで問題を解決(非同期または同期)する方法は、呼び出し側に隠されていません(抽象化されていません)。呼び出し元は、同期か非同期かを知る必要があります。

「良いアーキテクチャ」の原則と矛盾するベストプラクティスを非同期/待機していませんか?

これは、各インターフェイス(IEnumerable、IDataAccessLayerなど)が非同期の対応物(IAsyncEnumerable、IAsyncDataAccessLayer)を必要とすることを意味します。これにより、非同期の依存関係に切り替えるときにスタックで置き換えることができます。

問題をもう少しプッシュすると、すべてのメソッドが非同期であると仮定して(Task <>またはPromise <>を返すため)、メソッドが非同期呼び出しを実際には同期していないときに同期させるのが簡単になりますか?非同期?これは将来のプログラミング言語に期待されるものですか?

106
corentinaltepe

あなたの機能は何色ですか?

Bob Nystromの 関数の色 に興味があるかもしれません。1

この記事では、彼は架空の言語について説明します。

  • 各関数には、青または赤の色があります。
  • 赤の関数は青または赤の関数を呼び出すことができ、問題はありません。
  • Blue関数は、blue関数のみを呼び出すことができます。

これは架空のものですが、プログラミング言語ではかなり定期的に発生します。

  • C++では、 "const"メソッドはthisで他の "const"メソッドのみを呼び出すことができます。
  • Haskellでは、非IO関数は非IO関数のみを呼び出すことができます。
  • C#では、同期関数は同期関数のみを呼び出すことができます2

ご存知のように、これらのルールのため、赤い関数はコードベースの周りにspreadする傾向があります。 1つ挿入すると、コードベース全体が少しずつコロニー化します。

1 Bob Nystromは、ブログを除いて、Dartチームの一部でもあり、この小さなCrafting Interpretersシリーズを書いています。すべてのプログラミング言語/コンパイラの愛好家に強くお勧めします。

2 非同期関数を呼び出して、それが戻るまでブロックする可能性があるので、真実ではありませんが...

言語の制限

これは基本的に、言語/ランタイムの制限です。

たとえば、ErlangやGoなどのM:Nスレッドを使用する言語にはasync関数がありません。各関数は非同期である可能性があり、その「ファイバー」は一時停止、スワップアウト、およびスワップインされるだけでそれは再び準備ができています。

C#は1:1スレッドモデルを採用していたため、誤ってスレッドをブロックしないように、言語の同期性を表面化することにしました。

言語の制限がある場合、コーディングガイドラインを適応させる必要があります。

110
Matthieu M.

ここに矛盾がありますが、「ベストプラクティス」が悪いわけではありません。これは、非同期関数は本質的に同期関数とは異なる動作をするためです。依存関係(通常はIO)からの結果を待つ代わりに、メインイベントループによって処理されるタスクを作成します。これは、抽象化のもとで十分に隠せる違いではありません。

82
max630

非同期メソッドの動作は、同期メソッドとは異なります。ご存じのとおりと思います。実行時に非同期呼び出しを同期呼び出しに変換するのは簡単ですが、その逆は言えません。それで、それでロジックはそれから、それを必要とするかもしれないすべてのメソッドの非同期メソッドを作って、呼び出し側に必要に応じて同期メソッドに「変換」させてみませんか?

ある意味では、例外をスローするメソッドと、「安全」でエラーが発生してもスローしないメソッドを持つようなものです。コーダーは、他の方法で変換できるこれらのメソッドを提供するために過剰なのはどの時点ですか?

これには2つの考え方があります。1つは複数のメソッドを作成することで、それぞれが別のおそらくプライベートメソッドを呼び出し、オプションのパラメーターを提供したり、非同期などの動作に小さな変更を加えたりする可能性があります。もう1つは、必要な変更を自分で実行するために呼び出し側に任せるように、インターフェイスメソッドを最小限に抑えることです。

あなたが最初の学校の人なら、すべての呼び出しが2倍になるのを避けるために、同期呼び出しと非同期呼び出しにクラスを専念させる特定のロジックがあります。 Microsoftはこの考え方を支持する傾向があり、慣例により、Microsoftが好むスタイルとの一貫性を保つには、インターフェイスがほとんど常に「I」で始まるのと同じように、非同期バージョンも必要になるでしょう。それが-間違っているではないことを強調しておきます。それは、「正しい方法」で行うよりもプロジェクトで一貫したスタイルを維持し、開発のスタイルを根本的に変更する方が良いためです。プロジェクトに追加します。

とはいえ、私は2番目の学校を好む傾向があります。これは、インターフェースメソッドを最小限に抑えることです。メソッドが非同期に呼び出される可能性があると思う場合、メソッドは非同期です。呼び出し元は、続行する前にそのタスクが完了するのを待つかどうかを決定できます。このインターフェイスがライブラリへのインターフェイスである場合、非推奨または調整する必要のあるメソッドの数を最小限に抑えるために、この方法を実行する方が合理的です。インターフェイスがプロジェクトで内部的に使用される場合、提供されたパラメーターのためにプロジェクト全体で必要なすべての呼び出しにメソッドを追加し、「追加」メソッドは追加しません。それでも、メソッドの動作がまだカバーされていない場合のみ既存の方法で。

しかし、この分野の多くのことのように、それは主に主観的です。どちらのアプローチにも長所と短所があります。マイクロソフトはまた、変数名の先頭にタイプを示す文字を追加する規則と、それがメンバーであることを示す「m_」を追加し、m_pUser。私の要点は、マイクロソフトでさえ間違いがないことであり、間違いも犯す可能性があるということです。

そうは言っても、プロジェクトがこの非同期規約に従っている場合は、それを尊重してスタイルを継続することをお勧めします。そして、あなた自身のプロジェクトが与えられて初めて、あなたは自分に合った最良の方法でそれを書くことができます。

6
Neil

シグネチャを変更せずに非同期で関数を呼び出すことができる方法があると想像してみましょう。

それは本当にクールで、誰もあなたが彼らの名前を変えることを勧めないでしょう。

ただし、実際の非同期関数は、別の非同期関数を待機する関数だけでなく、最下位レベルにも、非同期の性質に固有の構造があります。例えば

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Taskasyncのようなものがカプセル化する非同期の方法でコードを実行するために呼び出しコードが必要とするのは、これらの2ビットの情報です。

非同期関数を実行する方法は複数あります。async Taskは、戻り値の型を介してコンパイラに組み込まれた1つのパターンにすぎないため、手動でイベントをリンクする必要はありません。

2
Ewan

C#nessの少ない、より一般的な方法で要点を説明します。

「良いアーキテクチャ」の原則と矛盾するベストプラクティスを非同期/待機していませんか?

それは、APIの設計での選択と、ユーザーに何を許可するかに依存するだけだと思います。

APIの1つの関数を非同期のみにしたい場合は、命名規則に従うことにほとんど関心がありません。常に戻り値の型としてTask <>/Promise <>/Future <>/...を返すだけで、それは自己文書化されます。同期の答えが必要な場合でも、彼は待機することでそれを行うことができますが、常にそうする場合、それは定型的なものになります。

ただし、APIのみを同期する場合、つまり、ユーザーがそれを非同期にしたい場合は、ユーザーが自分で非同期部分を管理する必要があります。

これにより、かなり多くの追加作業が発生する可能性がありますが、ユーザーが許可する同時呼び出しの数、タイムアウトの設定、再試行などについて、ユーザーにより多くの制御を提供することもできます。

巨大なAPIを備えた大規模なシステムでは、それらのほとんどをデフォルトで同期するように実装する方が、特にリソース(ファイルシステム、CPU、データベースなど)を共有する場合、APIの各部分を個別に管理するよりも簡単で効率的です。

実際、最も複雑な部分については、APIの同じ部分の2つの実装を完全に用意できます。1つは便利な処理を実行する同期、もう1つは同期に依存して処理を処理し、同時実行、ロード、タイムアウト、および再試行のみを管理します。 。

多分私はそのようなシステムの経験がないので、誰かが彼の経験をそれと共有することができます。

0
Walfrat