web-dev-qa-db-ja.com

プログラミング言語が同期/非同期問題を自動的に管理しないのはなぜですか?

私はこれについて多くのリソースを見つけていません:非同期のコードを同期的な方法で書くことが可能である/良いアイデアであるかどうか疑問に思っていました。

たとえば、次のJavaScriptコードは、データベースに保存されているユーザーの数を取得します(非同期操作)。

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

このようなものを書くことができればいいのですが:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

そのため、コンパイラは自動的に応答を待機し、次にconsole.logを実行します。結果は他の場所で使用する必要がある前に、常に非同期操作が完了するのを待ちます。コールバックの約束、非同期/待機などをあまり使用しないため、操作の結果がすぐに利用できるかどうかを心配する必要はありません。

エラーは、try/catch、または Swift 言語のようなオプションのようなものを使用して管理できます(nbOfUsersは整数またはエラーを取得しましたか?)。

出来ますか?ひどい考えかもしれないし、ユートピアかも…。

29
Cinn

Async/awaitは、2つの追加キーワードがありますが、まさに提案した自動管理です。なぜ重要なのですか?後方互換性は別として?

  • コルーチンが一時停止および再開される可能性のある明示的なポイントがない場合、待機可能な値を待機する必要がある場所を検出するためにtype systemが必要になります。多くのプログラミング言語には、そのような型システムはありません。

  • 値を待機することを明示的にすることで、待機可能な値をファーストクラスオブジェクトとして渡すこともできます。これは、高次のコードを書くときに非常に役立ちます。

  • 非同期コードは、言語の例外の不在や存在と同様に、言語の実行モデルに非常に大きな影響を与えます。特に、非同期関数は、非同期関数によってのみ待機できます。これはすべての呼び出し関数に影響します!しかし、この依存関係チェーンの最後で関数を非同期から非同期に変更するとどうなるでしょうか。これは、下位互換性のない変更です。すべての関数が非同期で、すべての関数呼び出しがデフォルトで待機されている場合を除きます。

    そして、それはパフォーマンスに非常に悪い影響を与えるため、非常に望ましくありません。単純に安い値を返すことはできません。すべての関数呼び出しははるかに高価になります。

非同期は素晴らしいですが、ある種の暗黙の非同期は実際には機能しません。

Haskellのような純粋な関数型言語では、実行順序がほとんど指定されておらず、監視できないため、エスケープハッチが少しあります。または別の言い方をすると、操作の特定の順序は明示的にエンコードする必要があります。これは、実際のプログラム、特に非同期コードが非常に適しているI/O負荷の高いプログラムでは、かなり扱いにくい場合があります。

65
amon

あなたが欠けているのは、非同期操作の目的です:それらはあなたがあなたの待ち時間を利用することを可能にします!

サーバーからリソースを要求するなどの非同期操作を、暗黙的かつ即座に応答を待機することによって同期操作に変換すると、スレッドは待機時間。サーバーの応答に10ミリ秒かかる場合、約3,000万CPUサイクルが無駄になります。レスポンスのレイテンシがリクエストの実行時間になります。

プログラマーが非同期操作を発明した唯一の理由本質的に長時間実行されるタスクの待ち時間を他の有用な計算の背後に隠すためです。待機時間を有効な作業で満たすことができれば、CPU時間を節約できます。できない場合は、操作が非同期であっても何も失われません。

そのため、言語が提供する非同期操作を採用することをお勧めします。彼らはあなたの時間を節約するためにそこにあります。

一部はします。

Asyncは比較的新しい機能であるため、主流ではありません(まだ)。今のところ、それが優れた機能であるかどうか、または、フレンドリーで使いやすい方法でプログラマーに提示する方法について、良い感触を得たばかりです。表現力豊か/その他既存の非同期機能は既存の言語にほとんど組み込まれているため、少し異なる設計アプローチが必要です。

そうは言っても、どこでも明確にするのは良い考えではありません。一般的な失敗は、非同期呼び出しをループで実行し、実行を効率的にシリアル化することです。非同期呼び出しを暗黙的にすると、そのようなエラーがわかりにくくなる場合があります。また、Task<T>(または同等の言語)からTへの暗黙の強制型変換をサポートしている場合、2つのうちどちらが不明であるかがタイプチェッカーとエラー報告に多少の複雑さ/コストを追加する可能性があります。プログラマーは本当に欲しかった。

しかし、それらは乗り越えられない問題ではありません。その動作をサポートしたい場合は、ほぼ間違いなく可能ですが、トレードオフがあります。

13
Telastyn

これを行う言語があります。しかし、既存の言語機能で簡単に実現できるため、実際にはそれほど必要ではありません。

非同期性を表現するsome方法がある限り、FuturesまたはPromises純粋にライブラリ機能として、特別な言語機能は必要ありません。そして、(= /// =)Transparent Proxies表現する限り、2つの機能を組み合わせて透明な先物

たとえば、Smalltalkとその子孫では、オブジェクトはそのアイデンティティを変更でき、文字通り別のオブジェクトに「なる」ことができます(実際、これを行うメソッドはObject>>become:と呼ばれます)。

Future<Int>を返す長時間実行の計算を想像してください。このFuture<Int>は、実装が異なることを除いて、Intとすべて同じメソッドを持っています。 Future<Int>+メソッドは別の数値を追加せず、結果を返します。計算をラップする新しいFuture<Int>を返します。等々。 Future<Int>を返すことによって適切に実装できないメソッドは、代わりに結果を自動的にawaitし、次にself become: result.を呼び出します。これにより、現在実行中のオブジェクト(self 、つまりFuture<Int>)は文字どおりresultオブジェクトになります。つまり、これからは、これまでFuture<Int>であったオブジェクト参照は、どこでもIntになり、完全に透過になりますクライアントに。

特別な非同期関連の言語機能は必要ありません。

12
Jörg W Mittag

彼らは(まあ、それらのほとんど)。探している機能はthreadsと呼ばれます。

ただし、スレッドには独自の問題があります。

  1. コードは任意のポイントで一時停止できるため、everは「それ自体」で変更されないと想定することはできません。スレッドを使用してプログラミングする場合、プログラムは変化するものにどのように対処すべきかを考えるのに多くの時間を費やします。

    ゲームサーバーが他のプレイヤーに対するプレイヤーの攻撃を処理していると想像してください。このようなもの:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    3か月後、プレイヤーは殺されて正確にログオフすることで、attacker.addInventoryItemsが実行中、次にvictim.removeInventoryItemsは失敗し、アイテムを保持でき、攻撃者はアイテムのコピーも取得します。彼はこれを数回行い、薄い空気から100万トンの金を生み出し、ゲームの経済を破壊しました。

    あるいは、ゲームが犠牲者にメッセージを送信している間に攻撃者がログアウトすることができ、彼の頭の上に「殺人者」タグが付けられないため、次の犠牲者が逃げることはありません。

  2. コードはany pointで一時停止できるため、データ構造を操作するときはどこでもロックを使用する必要があります。ゲームで明らかな結果をもたらす上記の例を示しましたが、もっと微妙な場合もあります。リンクされたリストの先頭にアイテムを追加することを検討してください:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    スレッドを一時停止できるのは、スレッドがI/Oを実行しているときだけであり、どの時点でも中断できないと言う場合、これは問題ではありません。しかし、ロギングなどのI/O操作がある状況を想像できると思います。

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. コードは任意のポイントで一時停止できるため、保存する状態が多くなる可能性があります。システムは、各スレッドに完全に別個のスタックを提供することでこれに対処します。ただし、スタックは非常に大きいため、32ビットプログラムでは約2000スレッドを超えることはできません。または、スタックサイズが小さすぎる危険性があるため、スタックサイズを小さくすることもできます。

7
user253751

質問は文字通り非同期プログラミングについてであり、非ブロッキングIOについてではないので、ここでは多くの答えが誤解を招くので、この特定のケースでは、もう1つを議論せずに1つを議論することはできないと思います。

非同期プログラミングは本質的には非同期ですが、非同期プログラミングの存在理由は、主にカーネルスレッドのブロックを回避することです。 Node.jsは、コールバックまたはPromisesを介して非同期性を使用し、ブロッキングオペレーションをイベントループからディスパッチできるようにします。JavaのNettyは、コールバックまたはCompletableFuturesを介して非同期性を使用します同様のことをしてください。

ただし、非ブロッキングコードは非同期性を必要としません。それは、プログラミング言語とランタイムがどれだけ進んでくれるかによって異なります。

Go、Erlang、Haskell/GHCがこれを処理できます。 var response = http.get('example.com/test')のようなものを記述して、応答を待つ間に舞台裏でカーネルスレッドを解放させることができます。これは、goroutine、Erlangプロセス、またはブロック時にカーネルスレッドをバックグラウンドで解放するforkIOによって行われ、応答を待っている間に他のことを実行できます。

言語が非同期性を実際に処理できないことは事実ですが、一部の抽象化では、他のものよりも先に進むことができます。区切りなしの継続または非対称コルーチン。ただし、非同期コードの主な原因であるシステムコールのブロックは、絶対にcanは開発者から抽象化されます。

Node.jsとJava --非同期非ブロッキングコードをサポートしますが、GoとErlangは同期非ブロッキングコードをサポートします。どちらもコードです。さまざまなトレードオフのある有効なアプローチ。

私のどちらかといえば主観的な議論は、開発者に代わって非ブロッキングを管理するランタイムに対して反対する人は、初期の問題におけるガベージコレクションに対して反対する人と同じだということです。はい、コストがかかります(この場合、主にメモリが多くなります)が、開発とデバッグが容易になり、コードベースがより堅牢になります。

私は個人的に、非同期の非ブロッキングコードは将来のシステムプログラミング用に予約する必要があり、より現代的なテクノロジースタックは同期の非ブロッキングアプリケーション開発用のランタイムに移行する必要があると主張します。

4
Louis Jackman

私があなたを正しく読んでいるなら、あなたは同期プログラミングモデルを求めていますが、高性能の実装です。それが正しければ、グリーンスレッドやプロセスなどの形ですでに利用できます。 ErlangまたはHaskell。そう、それは素晴らしいアイデアですが、既存の言語への改造は必ずしもあなたが望むほどスムーズではありません。

3
monocell

私はその質問に感謝し、回答の大部分は現状を単に守っているだけだと思っています。低レベルから高レベルの言語の範囲で、私たちはしばらくの間、わだち掘れに悩まされてきました。次のより高いレベルは明らかに、構文(awaitやasyncなどの明示的なキーワードの必要性)に重点が置かれておらず、意図についてもはるかに重視されている言語になるでしょう。 (Charles Simonyiへの明らかな功績ですが、2019年と将来について考えています。)

プログラマーに言った場合、データベースから値をフェッチするだけのコードを書いても、「そしてBTWはUIをハングさせないでください」、「バグを見つけるのを困難にする他の考慮事項を導入しないでください」 」次世代の言語とツールを備えた将来のプログラマーは、1行のコードで値をフェッチしてそこから移動するだけのコードを書くことができるでしょう。

最高レベルの言語は英語を話すことであり、あなたが本当にやりたいことを知るためにタスク実行者の能力に依存しています。 (スタートレックのコンピューターを考えるか、Alexaに何かを尋ねてください。)私たちはそれからは程遠いですが、近づいてきました。私の期待は、言語/コンパイラーが堅牢で意図的なコードを生成するために、より遠くに行かなくてもよいということですAIが必要です。

一方では、これを行うScratchのような新しい視覚言語があり、すべての構文上の専門知識にとらわれていません。確かに、舞台裏では多くの作業が行われているため、プログラマは心配する必要がありません。とは言っても、私はビジネスクラスのソフトウェアをScratchで作成しているわけではないので、成熟したプログラミング言語が同期/非同期の問題を自動的に管理する時が来たと同じ期待を持っています。

2
Mikey Wetzel

あなたが説明している問題は2つあります。

  • 作成しているプログラムは、全体として非同期で動作する必要があります外部から見た場合
  • 関数呼び出しが制御を放棄するかどうかに関係なく、呼び出しサイトでnotを表示する必要があります。

これを達成するにはいくつかの方法がありますが、基本的には

  1. 複数のスレッドを持つ(抽象化のあるレベルで)
  2. 言語レベルで複数の種類の関数があり、それらはすべてfoo(4, 7, bar, quux)のように呼び出されます。

(1)については、複数のプロセスのforkと実行、複数のカーネルスレッドの生成、および言語ランタイムレベルのスレッドをカーネルスレッドにスケジュールするグリーンスレッドの実装をひとまとめにしています。問題の観点から、それらは同じです。この世界では、スレッドの観点から制御をあきらめたり、制御を失う関数はありませんスレッド自体は制御できない場合や実行されない場合がありますが、この世界では自分のスレッドの制御をあきらめません。このモデルに適合するシステムは、新しいスレッドを生成したり、既存のスレッドに参加したりできる場合とそうでない場合があります。このモデルに適合するシステムは、Unixのforkのようなスレッドを複製する機能を備えている場合と備えていない場合があります。

(2)面白いです。それを正義にするために、私たちは導入と除去のフォームについて話す必要があります。

暗黙のawaitを、下位互換性のある方法でJavascriptなどの言語に追加できない理由を示します。基本的な考え方は、ユーザーにプロミスを公開し、同期コンテキストと非同期コンテキストを区別することにより、JavaScriptが実装の詳細を漏らし、同期関数と非同期関数を均一に処理できないということです。非同期関数本体の外ではプロミスをawaitできないという事実もあります。これらの設計の選択は、「呼び出し側から非同期性を見えなくする」ことと互換性がありません。

ラムダを使用して同期関数を導入し、関数呼び出しでそれを排除できます。

同期機能の紹介:

((x) => {return x + x;})

同期機能の削除:

f(4)

((x) => {return x + x;})(4)

これは、非同期関数の導入と削除と対照的です。

非同期機能の紹介

(async (x) => {return x + x;})

非同期関数の削除(注:async関数内でのみ有効)

await (async (x) => {return x + x;})(4)

ここでの根本的な問題は非同期関数はpromiseオブジェクトを生成する同期関数でもあるです。

以下は、node.js replで非同期関数を同期的に呼び出す例です。

> (async (x) => {return x + x;})(4)
Promise { 8 }

仮に、動的に型付けされた言語であっても、非同期関数呼び出しと同期関数呼び出しの違いは呼び出しサイトでは表示されず、定義サイトでも表示されない可能性があります。

そのような言語を取り、それをJavaScriptに下げることは可能ですが、すべての関数を効果的に非同期にする必要があります。

1
Gregory Nisbet

Go言語のゴルーチンとGo言語のランタイムを使用すると、すべてのコードを同期のように書くことができます。操作が1つのゴルーチンでブロックされた場合、実行は他のゴルーチンで続行されます。チャネルを使用すると、ゴルーチン間で簡単に通信できます。これは、JavaScriptや他の言語のasync/awaitなどのコールバックよりも簡単です。いくつかの例と説明については https://tour.golang.org/concurrency/1 を参照してください。

さらに、個人的な経験はありませんが、Erlangにも同様の機能があると聞いています。

ですから、はい、同期や非同期の問題を解決するGoやErlangのようなプログラミング言語がありますが、残念ながらそれらはまだあまり普及していません。これらの言語の人気が高まるにつれ、おそらくそれらが提供する機能は他の言語でも実装されるでしょう。

1
user332375

まだ提起されていない非常に重要な側面があります:再入可能です。非同期呼び出し中に実行される他のコード(つまり、イベントループ)がある場合(そして、そうでない場合、なぜ非同期が必要なのでしょうか?)、コードはプログラムの状態に影響を与える可能性があります。呼び出し元からの非同期呼び出しを非表示にすることはできません。これは、呼び出し元が関数の呼び出しの間、影響を受けないようにするためにプログラムの状態の一部に依存している可能性があるためです。例:

_function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}
_

bar()が非同期関数の場合、その実行中に_obj.x_が変更される可能性があります。これは、バーが非同期であり、その効果が可能であるというヒントがなければ、予想外です。唯一の代替策は、考えられるすべての関数/メソッドが非同期であると疑い、各関数呼び出しの後にローカルでない状態を再フェッチして再チェックすることです。これは微妙なバグが発生する傾向があり、一部の非ローカル状態が関数を介してフェッチされる場合は、まったく不可能である可能性もあります。そのため、プログラマーは、どの関数がプログラムの状態を予期しない方法で変更する可能性があるかを認識する必要があります。

_async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}
_

これで、bar()が非同期関数であることがはっきりとわかります。これを処理する正しい方法は、後で_obj.x_の期待値を再確認して、発生した可能性のある変更に対処することです。発生した。

他の回答ですでに述べたように、Haskellのような純粋な関数型言語は、共有/グローバル状態をまったく必要としないことで、その影響を完全に回避できます。私は関数型言語の経験があまりないので、たぶんそれに偏っていますが、より大きなアプリケーションを作成するときは、グローバルな状態がないことが利点だとは思いません。

1
j_kubik

質問で使用したJavascriptの場合、注意すべき重要な点があります。Javascriptはシングルスレッドであり、非同期呼び出しがない限り、実行順序は保証されます。

したがって、次のようなシーケンスがある場合:

const nbOfUsers = getNbOfUsers();

その間、他に何も実行されないことが保証されます。ロックや同様のものは必要ありません。

ただし、getNbOfUsersが非同期の場合は、次のようになります。

const nbOfUsers = await getNbOfUsers();

は、getNbOfUsersの実行中に、実行が生成され、その間に他のコードが実行される可能性があることを意味します。これは、あなたが何をしているのかに応じて、いくつかのロックを発生させる必要があるかもしれません。

したがって、呼び出しが非同期である場合とそうでない場合があることに注意することをお勧めします。状況によっては、呼び出しが同期である場合には不要な追加の予防策を講じる必要があるためです。

0
jcaron