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


103

この質問はC#言語に関するものですが、JavaやTypeScriptなどの他の言語をカバーするものと期待しています。

Microsoft は、.NETで非同期呼び出しを使用する場合のベストプラクティスを推奨しています。これらの推奨事項のうち、2つを選択しましょう。

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

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

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

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

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

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


5
これは素晴らしい議論の質問のように聞こえますが、ここでは答えるにはあまりにも意見に基づいていると思います。
陶酔

22
@Euphoric:ここで取り上げた問題は、C#ガイドラインよりも深くなると思います。これは、アプリケーションの一部を非同期動作に変更すると、システム全体に非ローカルな影響を与える可能性があるという事実の単なる兆候です。したがって、私の腸は、技術的な事実に基づいて、これに対する非意見の答えがなければならないことを教えてくれます。したがって、私はここにいる全員にこの質問を早めに閉じないようお勧めします。
ドックブラウン

25
@DocBrownここでのより深い質問は、「システムの一部を同期から非同期に変更することはできますか?その答えは明確な「いいえ」だと思います。その場合、ここでは「優れたアーキテクチャと階層化の概念」がどのように適用されるかわかりません。
陶酔

6
@Euphoric:非意見の答えの良い基礎のように聞こえる;-)
Doc Brown

5
@Gherman:C#は、多くの言語と同様に、戻り値の型のみに基づいてオーバーロードを行うことができないためです。同期メソッドと同じシグネチャを持つ非同期メソッドが作成されます(すべてがを取得できるわけではなくCancellationToken、デフォルトを提供したいメソッドもあります)。既存の同期メソッドを削除する(およびすべてのコードを積極的に破壊する)ことは、明らかに非スターターです。
Jeroen Mostert

回答:


111

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

ボブ・ナイストロムの「What Color Is Your Function 1 」に興味があるかもしれません。

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

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

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

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

気づいたように、これらのルールのために、赤い関数はコードベース全体に広がる傾向があります。挿入すると、コードベース全体に少しずつ定着します。

1 ボブ・ナイストロムは、ブログとは別にDartチームの一員であり、この小さなCrafting Interpretersシリーズを書きました。すべてのプログラミング言語/コンパイラー愛好家に強くお勧めします。

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

言語の制限

これは、本質的に、言語/実行時の制限です。

たとえば、ErlangやGoなどのM:Nスレッドを使用する言語にはasync関数がありません。各関数は非同期である可能性があり、その「ファイバー」は単に準備ができたら一時停止、交換、交換されます。

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

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


4
IO関数は拡散する傾向がありますが、熱心に取り組むと、コードのエントリポイントの近く(呼び出し時のスタック内)の関数にほとんど分離できます。これを行うには、これらの関数にIO関数を呼び出してから、他の関数にその関数からの出力を処理させ、さらにIOに必要な結果を返すようにします。このスタイルにより、コードベースの管理と操作がはるかに簡単になります。共時性の帰結があるのだろうか。
jpmc26

16
「M:N」および「1:1」スレッドとはどういう意味ですか?
キャプテンマン

14
@CaptainMan:1:1スレッドとは、1つのアプリケーションスレッドを1つのOSスレッドにマッピングすることです。これは、C、C ++、Java、C#などの言語の場合です。対照的に、M:Nスレッドは、MアプリケーションスレッドをN OSスレッドにマッピングすることを意味します。Goの場合、アプリケーションスレッドは「goroutine」と呼ばれ、Erlangの場合、「actor」と呼ばれます。また、「グリーンスレッド」または「ファイバー」と聞いたことがあるかもしれません。並列性を必要とせずに並行性を提供します。残念ながら、このトピックに関するウィキペディアの記事はかなりまばらです。
マチューM.

2
これはやや関連していますが、この「関数の色」の考え方は、ユーザーからの入力をブロックする関数にも適用されると思います。たとえば、モーダルダイアログボックス、メッセージボックス、コンソールI / Oのフォームなどです。フレームワークは最初から持っています。
jrh

2
なつみ C#には、1つのOSスレッドごとに1つのアプリケーションスレッドがありません。これは、ネイティブコードを操作しているとき、特に MS SQLで実行しているときは明らかです。そしてもちろん、協調的なルーチンは常に可能でした(そして、さらに簡単asyncです)。実際、レスポンシブUIを構築するための非常に一般的なパターンでした。アーランと同じくらいきれいでしたか?いや。しかし、それはまだCから遠いです:)
ルアーン

82

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


27
答えはこのIMOと同じくらい簡単です。同期プロセスと非同期プロセスの違いは実装の詳細ではなく、意味的に異なるコントラクトです。
Ant P

11
@AntP:そんなに簡単だとは思わない。C#言語で表示されますが、たとえばGo言語では表示されません。そのため、これは非同期プロセスに固有のプロパティではなく、非同期プロセスが特定の言語でどのようにモデル化されるかという問題です。
マチューM.

1
なつみ はい。ただしasync、C#のメソッドを使用して、必要に応じて同期コントラクトを提供することもできます。唯一の違いは、Goはデフォルトで非同期ですが、C#はデフォルトで同期することです。async2番目のプログラミングモデルを提供します- async 抽象化です(実際に行うことは、ランタイム、タスクスケジューラ、同期コンテキスト、awaiterの実装に依存します...)。
ルアーン

6

非同期メソッドは、同期メソッドとは異なる動作をすることを知っています。実行時に、非同期呼び出しを同期呼び出しに変換することは簡単ですが、その反対は言えません。したがって、ロジックは次のようになります。なぜそれを必要とする可能性のあるすべてのメソッドの非同期メソッドを作成し、呼び出し元に必要に応じて同期メソッドに「変換」させないのでしょうか。

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

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

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

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

ただし、この分野の多くのものと同様に、それは主に主観的です。どちらのアプローチにも長所と短所があります。Microsoftは、変数名の先頭に型を示す文字を追加し、それがメンバーであることを示す「m_」を追加して、などの変数名を追加する規則も開始しましたm_pUser。私のポイントは、Microsoftでさえ間違いないわけではなく、間違いも犯す可能性があるということです。

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


6
「実行時に、非同期呼び出しを同期呼び出しに変換するのは簡単です」私はそれが正確にそうかどうかはわかりません。.NETでは、.Wait()methodなどを使用するとマイナスの結果が生じる可能性があり、jsでは、私が知る限り、それはまったく不可能です。
max630

2
@ max630考慮すべき同時発生の問題がないとは言いませんでしたが、最初に同期タスクであった場合、デッドロックを作成していない可能性があります。とはいえ、些細なことは「ダブルクリックして同期に変換する」という意味ではありません。jsでは、Promiseインスタンスを返し、その上でresolveを呼び出します。
ニール

2
ええお尻のその完全に痛みが同期に戻って非同期に変換するには
ユアン・

4
@Neil javascriptでは、呼び出してPromise.resolve(x)からコールバックを追加しても、それらのコールバックはすぐには実行されません。
NickL

1
@Neilは、インターフェイスが非同期メソッドを公開する場合、タスクで待機してもデッドロックが発生しないと想定するのは適切な仮定ではありません。インターフェースが、メソッドシグネチャで実際に同期していることを示す方が、後のバージョンで変更される可能性があるドキュメントの約束よりもはるかに優れています。
カールウォルシュ

2

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

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

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

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つなので、イベントを手動でリンクする必要はありません。


0

私は、C#nessをより少なく、より一般的な方法で主要なポイントに対処します。

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

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

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

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

これにより、非常に多くの余分な作業を行うことができますが、同時に許可する同時呼び出しの数、タイムアウトの設定、再試行などについて、ユーザーにより多くの制御を与えることもできます。

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

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

私はそのようなシステムの経験が足りないので、誰か他の人が彼の経験を共有できるかもしれません。


2
@Miral両方の可能性で「同期メソッドから非同期メソッドを呼び出す」を使用しました。
エイドリアンラグ

@AdrianWraggだから私はやった。私の脳は競合状態にあったに違いありません。直します。
ミラル

それは逆です。同期メソッドから非同期メソッドを呼び出すのは簡単ですが、非同期メソッドから同期メソッドを呼び出すことは不可能です。(そして、物事が完全にバラバラになるのは、誰かがいずれにせよ後者を行おうとすることであり、それがデッドロックを引き起こす可能性があります。)残念ながら、非同期実装は非同期メソッドしか呼び出すことができないため、これも難しい選択です。
ミラル

(そしてこれにより、もちろんブロック同期メソッドを意味します。非同期メソッドから純粋にCPUバウンドの計算を同期的に行うものを呼び出すことができます。 UIコンテキストではなく、ロックまたはI / Oまたは別の操作のためにアイドル状態で待機する呼び出しをブロックすることは悪い考えです。)
Miral
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.