世界を動かす技術を、日本語で。

非同期性は同時実行性ではない

概要

AsynchronyConcurrencyParallelism の違いを明確に解説 「非同期」と「並行」の混同がもたらす問題点の指摘 Zig言語での io.async の具体的な挙動 従来のライブラリ設計・利用の課題と改善策の提案 async コードの伝播問題や設計上のベストプラクティスを紹介

非同期は並行性ではない:Zigにおける設計思想

  • 多くの人が「 Concurrency is not Parallelism」と言うが、 asynchrony という概念が抜け落ちている現状
  • Wikipedia の定義
    • Concurrency :複数タスクを同時実行またはタイムシェアリングで進行できる能力
    • Parallel computing :複数の計算や処理を物理的に同時実行する能力
  • 本質的な違いを理解しないことでソフトウェア設計に悪影響
  • asynchrony :タスクの実行順序が前後しても正しさが保たれる特性
  • concurrency :タスクを複数同時に進行できる能力(並列またはタスク切り替えによる)
  • parallelism :物理的に複数タスクを同時実行できる能力

ファイル保存とソケット接続の例

  • 2つのファイルを保存する場合、どちらを先に書いても、また交互に書いても正しい( asynchrony
  • サーバーとクライアントのソケット接続では、両者の実行が重なる必要がある( concurrency 必須)
  • 前者は順序の自由度があるが、後者は同時進行しなければ成立しない

なぜこの違いが重要か

  • asynchronyconcurrency の区別が曖昧なため、
    • ライブラリ作者が同じ機能を非同期・同期で二重実装する羽目になる
    • ユーザーも「asyncコードは伝染する」現象に悩まされる
    • 無理な回避策でデッドロックやパフォーマンス劣化を招く
  • 正しく区別することで、
    • ライブラリの重複や「async専用」設計の必要がなくなる
    • 通常の同期コードと非同期コードが共存可能

Zigにおける asynchrony と concurrency

  • io.async は concurrency を必ずしも意味しない
    • 単一スレッドのブロッキングモードでも動作可能
    • 例:
      • io.async(saveFileA, .{io}); io.async(saveFileB, .{io});
      • 単一スレッドなら saveFileA(io); saveFileB(io); と等価
  • io.async を使っても、ユーザーは同期I/Oのまま利用可能
  • 逆に、 io.async を使わなくても concurrency を利用可能
  • concurrency の本質は、
    • イベント駆動I/Oシステムコール(io_uring, epollなど)の利用
    • タスク切り替えプリミティブ(yield等)の利用
  • asyncはasynchronyのためのものであり、task switchingはconcurrencyのためのもの

タスク切り替え(yield)の仕組み

  • グリーンスレッドの場合、yieldはスタックスワッピングで実現
    • CPUレジスタやスタックの状態を保存・復元
    • OSのスレッドスケジューリングと同様の仕組み
  • イベントループはI/O完了を待つ間、他のタスクに切り替え
  • これにより、同期的に書かれたコードも並行的に実行可能

同期・非同期コードの共存

  • saveDataのような同期的な関数も、io.asyncで包めば非同期的にスケジューリング可能
  • 通常の同期コードと非同期コードが同一プログラム内で混在・共存
  • Go言語のgoroutineの例のように、async/awaitキーワードの伝播問題が発生しにくい設計

concurrencyが必須となるケース

  • サーバーacceptとクライアントconnectのように、両タスクの重なりが必須な場合
  • Zigでは io.asyncConcurrent を使い、明示的にconcurrencyを保証
    • concurrencyを必要とすることをコード上で明示
    • 非concurrentなIo実装で実行時エラーを発生させることで安全性を担保
  • io.async でエラー発生時は関数を直接実行するフォールバック設計
    • リソース不足時の堅牢性向上

まとめ

  • asynchronyconcurrency の違いを明確に区別する重要性
  • Zigの設計思想により、同期・非同期・並行処理の柔軟な共存が可能
  • async/awaitの伝播問題やライブラリの二重実装の回避
  • 正しい抽象化でシンプルかつ堅牢な並行プログラミングを実現

Hackerたちの意見

個人的には、著者は同時実行の定義を混同していると思う。 https://lamport.azurewebsites.net/pubs/time-clocks.pdf

論文をリンクするんじゃなくて、もうちょっと詳しく説明してくれない? 定義はまあまあ良いと思ったけど。 > 非同期性:タスクが順不同で実行されても正しい可能性。 > 同時実行性:システムが複数のタスクを同時に進行させる能力、並列性やタスクの切り替えを通じて。 > 並列性:システムが物理レベルで複数のタスクを同時に実行する能力。

だから、僕はこの用語を完全に使うのをやめたんだ。話す相手みんなが違う理解をしてる気がするし、コミュニケーションに全く役立たなくなった。

著者は、自分がブログ記事で使っている用語の定義が存在することを理解している。彼は改訂された定義を提案しているんだ。新しい定義が正確であれば、問題ないと思う。それを採用するかどうかは読者次第だね。

ランポートの論文の半分の概念をほとんどの言語で表現できないことを忘れないで。スレッドを開始するときに、全体的な時計の順序や部分的な時計の順序について話すことはないよね。プロトコルを設計する時にTLA+でしかやらない。とはいえ、「Zigには非同期APIに、非同時実行で実行するとコンパイルエラーを投げる関数がある。Zigはそれを言わせてくれる」という新しい用語を表現する必要はないと思う。それを新しい理論を提案せずにやるのは全然問題ないよ。

新しいZigのI/Oアイデアは、主にアプリケーションを書く人にはかなり画期的なアイデアに思える。スタックレスコルーチンが必要ないならね。でも、このスタイルでライブラリを書くのはかなりエラーが出やすいと思う。ライブラリの作者が提供されるI/Oがシングルスレッドかマルチスレッドか、イベント駆動I/Oを使っているかどうかもわからないから。並行/非同期/並列/なんでもいいけど、コードを書くのはそれ自体で十分難しいのに、I/Oスタックについて完璧な知識があってもそうなんだ。ここでは、ライブラリの作者は外部から提供されるI/O実装に振り回されることになる。I/Oインターフェースが「小さなOS」の実装みたいなものになるとしたら、すべての潜在的な相互作用や動作の組み合わせをテストするのはかなり難しいかもしれない。インターフェースが提供するいくつかの非同期プリミティブが、実際に遭遇する面白いエッジケースに対処するには十分かどうかはわからない。このような幅広いI/O実装をサポートするためには、コードはかなり防御的で、基本的に最も並列的/同時実行的なI/Oが使われることを前提にしなければならないと思う。スタックレスコルーチンとこのアプローチを組み合わせるのも難しいだろうし、特に無駄にコルーチンを生成しないようにしたいなら、提供されるプリミティブではコルーチンの明示的なポーリングを表現できないみたいだから(仮にできたとしても、ほとんどの人はそんなコードを書く気にならないだろうし、結局「普通の」非同期/awaitコードみたいに見えるだけだと思う)。動的ディスパッチと組み合わせると、Zigは言語設計で少し高レベルに進んでいるように見える。最終的には良いフィットになるかもね。このアプローチを「妥協なし」と呼ぶのはかなり勇気がいることだと思うけど、まだ実際に使われてないからね。広いエコシステムで1〜2年使った後に言えることだと思う。時間が経てばわかるよ :)

このアプローチを「妥協なし」と呼ぶのはかなり勇気がいることだと思う。まだ実際には試されていないからね。君の言う通りだ。とはいえ、「Jai」が成功しているとされるものにかなり近いように見える(暗黙のIOコンテキストを使っているけど、明示的に渡されるものではない)。でも、それを実際に試されていると言えるかどうかは議論の余地があるよね…

コードはかなり防御的で、基本的には最も並行・同時実行のIOバージョンを使うことを前提にしなければならないと思う。まさにその通りだけど、なぜ誰もが異なる考えを持つと思うのか、同期と非同期の実行の両方をサポートすることが目標なのに?でも、非同期性がIOイベントハンドラーの低レベルでうまく行われていれば、どこでもこれらの原則に従って簡単に実装できるはずだよ。最悪でもコードが逐次実行される(つまり遅くなる)だけで、レースやデッドロックにはならないはず。

僕は、著者が単に同時実行の定義から実行のyieldの概念を引き抜いて、新しい「非同期性」という用語に持っていっただけだと思う。そして、その用語が必要だと主張してるけど、実際には同時実行の概念が壊れてしまう。確かにそうだけど、yieldできる能力なしに同時実行はあまり意味がないから、実際にはそれに内在していると思う。これはとても重要な概念だけど、新しい用語に分けることで混乱を招くだけだと思う。

純粋な1対1の並列性は、yieldを伴わない同時実行の一形態としてカウントするよ。でも、それ以外は、すべての非並列同時実行は、たとえそれが命令レベルであっても、何らかのリズムで実行をyieldしなければならないに同意する。 (例えば、CUDAでは、ワープ内の分岐するスレッドが命令の実行をインターリーブするので、一方の分岐が他方でブロックしようとする場合がある。)

著者は、単に同時実行の定義から実行の譲渡という概念を引っ張り出して、この新しい「非同期」という用語に持ち込んだだけだと思う。記事には正反対のことが書いてあるけどね。>(そしてタスクスイッチングは、上で述べた定義によれば、同時実行特有の概念です)

同時実行は譲渡を意味しないよ… 同期ロジックは何らかの同期を含むし、譲渡は同期の一つの方法かもしれない。それが君の言いたいことだと思う。非同期ロジックは、同期や譲渡なしで同時実行を実現してる。実際のところ、同時実行と非同期ロジックはフォン・ノイマンマシンには存在しないんだよね。

この文脈での非同期性は、リクエストの準備と提出を結果の収集から分離する抽象化だ。この抽象化により、複数のリクエストを提出してから、その結果を問い合わせ始めることが可能になる。この抽象化は、同時実装を可能にするが、必須ではない。ただし、抽象化の意図は同時実行があることだ。動機は、同時実行なしには実現できない特定の利点を得ることだ。一部の非同期抽象化は、いくつかの同時実行なしには実装できない。リクエスターがリクエストの完了を通知される方法が、完了キューでのブロッキングリクエストではなく、コールバックだと仮定しよう。今、はい、コールバックはリクエストスレッドのコンテキストで発行できるので、すべてがシングルスレッドになる。でも、リクエストスレッドが非再帰的ミューテックスを保持している場合、そのトリックはデッドロックを引き起こすことで明らかになる。つまり、シングルスレッドではうまくいかない非同期リクエスト抽象化があるということだ;1つの呼び出し元がミューテックスをロックし、2つ目の呼び出し元がリクエストを提出し、3つ目の呼び出し元がミューテックスをアンロックし、4つ目の完了コールバックが発生する。もしステップ2が同じスレッドでコールバックを生成したら、ステップ3には到達できない。実装は、リクエスターがそのステップに到達できるように、スレッドが3を待機するための最小限の同時実行を使用しなければならない。

完全に同意します。投稿のサーバー/クライアントの例は、進行できないプログラムの一例に過ぎませんが、あなたが挙げた別の例は同じ方法では解決できないもので、時間が経つにつれて発見される例はもっとたくさんあると思います。私の意見では、非同期を使用する場合は同時実行性を確保する必要があります。

「非同期」という言葉はこの概念にはあまり良くないし、実際には非常に明確に定義された数学的な言葉がある:可換性。いくつかの演算は可換(順序は関係ない:加算、乗算など)だけど、他のものは非可換(順序が関係する:減算、除算など)。io.asyncConcurrent(Server.accept, .{server, io}); io.async(Client.connect, .{client, io}); 通常、コード内の操作の順序は行番号で示される(最初の行は二番目の行の前に起こる、みたいな)。でも、非同期コードではこれが通用しないこともあるよね。だから、直感的には(ゾッとするけど).then(...)のパラダイムの方が良い結果が得られると思う。嫌だけど、知らない悪魔より知ってる悪魔の方がマシだよね。今のままだと、asyncConcurrent(...)はめちゃくちゃ混乱するし、このブログ記事を暗記しないとこのコードが何を意味するのか全く分からないよ。Zigは(個人的に好きなRustみたいに)新しいヒップなことに挑戦してるのは分かるけど、半分の時間は直感的じゃなくて混乱させるだけだよね。何とかして(非同期ベースの)可換性や操作の順序を実装するか(Rustのライフタイムみたいに?)、それとも人々がすでに慣れているものを使うべきだと思う。

厳密に言えば、可換性は(2項)演算に対して定義されているから、もし2つの非同期文(例えば接続/受け入れ)が可換だと言うなら、「どの演算の下で?」と尋ねざるを得ない。今のところ、私のベストな答えはバインド(>>=)演算子(偶然にもそのインスタンスの一つである.then(...)を含む)だけど、これはあくまで曖昧な直感に過ぎない。

可換性は、片方が完全に前か後ろにあるという点で、ずっと弱い主張だよ。例えばABはCと可換かもしれないけど、ABC=CABとは限らない。非同期の場合、ABC=ACB=CABが保証されるんだよね。(これに対する既存の数学用語があるかもしれないけど、私は知らない)

「非同期」という言葉はこの概念にはあまり良くないし、実際には非常に明確に定義された数学的な言葉がある:可換性。別の用語がこの概念を定義しているからといって、それがより良い言葉だとは思わない。「可換性」は、私の意見では、混乱しているように感じるし、聞こえるし、読むのも難しい。非同期の方がずっと受け入れやすいよ。

非同期性は部分的な順序付けも可能にします。二つの操作は特定の順序で終了する必要があるかもしれませんが、その順序で実行する必要はありません。例えば、引き算は可換ではありません。でも、残高と控除を二つの別々のクエリとして計算し、その結果を適切な順序で適用することができます。

書かれている通り、asyncConcurrent(...)はめちゃくちゃ混乱するし、このブログ記事を暗記しない限り、このコードが何を意味するのか全く分からないよ。Zigが新しいヒップスター的なことを色々試してるのは分かるけど、半分の時間はただ直感的じゃなくて混乱させるだけだよね。何とかして(Rustのライフタイムみたいに)非同期ベースの可換性や操作の順序を実装するか、もうみんなが慣れているものを使った方がいいと思う。賛成できないな。混乱するのは、ブログ記事を覚えておかないといけないからで、核心的なアイデアを内面化してしまえば全く混乱しないはず。問題は、アイデアを内面化する価値があるのかってことだよね。分からないけど、確実に内面化する人もいて、これを念頭に置いて色々やろうとする人もいるだろうし、しばらくしたらこの道がどこに繋がるのか見えてくると思う。その時に良いアイデアかどうか判断できるようになるはず。 > 「非同期性」はこの文脈では非常に悪い言葉で、数学的には「可換性」という非常に明確な用語がある。これに「可換性」を使うのはリスクがある。Zigには演算子があって、その中には可換なものもある。だから混乱すると思う。例えば、f() + g()と書いたら、加算は可換だからZigはf()とg()を並行して実行することができる。実行順序と可換性は別のことだよね。おそらく、可換演算子と非可換演算子を一つのものにまとめることもできるかもしれないけど、それが良いアイデアかどうかは分からないし、これは非同期性を実験するのとは全く別の問題だと思う。

通常、コード内の操作の順序は行番号で示されます。ループを除いて、ループは後ろに戻ることを許可し、一時的に他の局所的な線形操作にジャンプすることを許可する手続きがあります。非前向きなことをするための構文はたくさんあります。

一部の操作は可換です(順序は重要ではない:加算、乗算など)。面白い事実:加算においては順序が重要です。(広く異なる指数を持つ多くの浮動小数点数を加算する場合。)

そうだな、俺の直感では、これを達成するには(ゾッとするけど).then(...)のパラダイムの方がいいと思う。最悪だけど、知らない悪魔より知ってる悪魔の方がマシだよ。awaitの全体的なアイデアは、.then()の醜さなしに古い直感を使えるようにすることなんだ。f(); await g(); h()は、期待通りの実行順序になってる。

つまり「協調的マルチタスクは先取りマルチタスクではない」ということです。「非同期」という言葉の典型的な使い方は、_言語がシングルスレッド_で協調的マルチタスク(イールドポイント)とイベントベースであり、外部の計算がブロックせずに並行して実行され、結果をイベントとして報告することを意味します。マルチスレッドまたは同時実行の実行モデルで非同期性を持つ意味はありません。ブロッキングI/Oを使用しても、1つの実行スレッドがブロックされている間にプログラムが進行することができます。その場合、イールドポイントを明示的にする必要はありません。

確かにこれは最も一般的な使い方だけど、反例としてRust(またはC#、F#、OCaml 5+)を挙げるよ。これらはOSスレッドと非同期の両方をサポートしてる。OSスレッドはCPUバウンドなタスクに向いてて、非同期はI/Oバウンドなタスクに向いてる。非同期(またはGoスタイルのM:Nスケジューリング)の主な利点は、RAMがあれば、好きなだけタスクやファイバー、ゴルーチンを立ち上げられることだよ。OSスレッドを使う場合は、コンテキストスイッチでCPUが詰まらないようにレスポンシブにプールする必要がある。これは難しくはないけど、I/O以上のことをやると、面白いデッドロックに遭遇することがある。

非同期を定義するのは難しいです。私はJavaScriptで非同期を設計した多くの人の一人としてこれを書いています。この投稿の定義にはあまり同意できません:非同期だからといって正しいわけではありません。async/awaitを使うかどうかに関わらず、非同期コードではユーザーランドのレースコンディションが発生する可能性があります。私の最新の定義(まだ改善の余地があると思います)は、非同期とはコードが明示的に同時実行のために構造化されていることを意味します。このトピックについて最近もう少し書きました: https://yoric.github.io/post/quite-a-few-words-about-async/ .

俺はこの件に詳しくないけど、こう答えるかな:非同期コードは、ブロッキングだったコードを非ブロッキングにすることで、他のことが進行中でも完了できるようにするものだよ。俺は埋め込みループでよく作業するから、長時間ブロッキングするコードがI/Oを壊したり、目に見える/聞こえるドロップアウトを引き起こすことがあるから、これが明らかな答えだと思う。

俺にとって重要なのは、非同期性の抽象的な概念とそれがどう実装されるかを区別することだよ。後者はプログラミング言語の抽象レベルと、マシン内の技術的な調整手段の両方を指してる。最高レベルの抽象概念としては、単に同期の二重性だね。つまり、何らかの形で一緒に作業する必要がある二者(またはそれ以上)が同期していない状態を意味する。何かが終わった後に何が起こるかが分からない、または定義されていないということだ。そう考えると、この定義は難しくない。難しいのは、言語で設計された抽象的手段だ。理解したり、間違いなく使ったりするために必要な認知的努力の量だよ。

非同期を定義する必要があるのかもよくわからない。難しいし(実際そうだし)、一つのことに合わせることができないからね。問題は、「非同期」を定義することが役に立つのか?それともイベントループ?物理チップの領域には、真の並列処理を可能にする概念がたくさんあるはずだし、「ユーザーの指」や「クイックジョブ」、ジョブキューやブロッキング・ノンブロッキングAPIには全然問題ないよ。指はタッチイベントやマウスクリック、キーボードや一般的なユーザーの発動イベントを象徴していて、これをクイックジョブに合わせる必要がある。クイックジョブはすごく小さい(実行時間的に)ブロッキングなジョブで、ブラウザによってキューに入れられて目標に達するために使われる。ノンブロッキングAPIが好きなのは、時間のかかるジョブを基盤に任せて、やりたいこと(データをインデックスDBに保存する)に対してクイックジョブを書くだけで済むから。成功した場合や失敗した場合に何が起こるか(異なるクイックジョブ)も考えなきゃいけない。同期や非同期はあまり助けにならないけど、もちろん他の人がそれについて話すときや、自分が使うとき(あるいは自分の好きなAIコーダーが)非同期を見たりするのは理解しなきゃいけない。でも、結局ノンブロッキングAPIを意味するだけなんだよね。ただ、非同期プログラミングモデルは実際にはすごくブロッキングなクイックジョブを書くことになっていて、ノンブロッキングな性質はブロッキングジョブの小さくて原子的な性質(実行時間的に)にある。これを混沌とした非決定的なイベントに合わせようとしてる。指やブラウザ、その他のものでトリガーされるイベントにね。正直言うと、これは混乱してるけど同時に天才的だと思う。非同期やそのモデルを理解するのは本当に難しかった。これが何かを意味するとは思えないんだ。実際、イベント、ブロッキングジョブ、ジョブキュー、ノンブロッキングAPIのような異なる概念を使えば理解するのはとても簡単なんだけど。私たちが何をしているのか、他の人が何をしているのか(ブラウザのコードやOSなど)を知ることも重要だと思う。C++のコードがスレッドで並行モデルを宣言するのと似ていて、OSが決定するんだ。JSでは、ノンブロッキングAPIを使っていて、これは暗黙的におそらく並行モデルを宣言している。ブラウザやNodeなどがそれを使うべきだし、彼らはいつもそうしてると思う。一番大事なのは、ジョブを素早く、たぶん30-50ms以内に保つこと。ノンブロッキングAPIは素晴らしいから、ジョブは意図を宣言するだけで終わる。C++やRustのような言語は、実際のタスクを並行に実行したいことをOSに宣言するから、たとえOSに実際の物理スレッドが1つしかなくても、UIは応答性がある。なぜなら、OSがUIコードの実行と「実際のタスク」の実行(ネットワークやデータベースなど)を切り替えるから。だから、非同期プログラマーがやるべきことは、素晴らしいUXモデルを作って、イベントをクイックジョブに合わせることだよ。

二つのライブラリを持たなくて済むのは素晴らしいアイデアだと思うから、これには賛成だね。一般的に非同期コードについて気になるのは、テストをどうやって行うかだよ。今日テストに合格したら、本番で起こりうるすべてのシナリオや順序を再現できたかどうかを自信を持って知りたいから。もちろん、スレッドでも同じ問題があって、マルチスレッドプログラムを書くのはずっと難しくてデバッグも大変だよ。だから、俺は本当に必要だと感じたときだけスレッドを使う。実際の問題は、それを開発者に伝えることなんだ。最近、Pythonのシステムで作業したとき、開発者たちが明らかに半分はJavaScriptをやってたんだ。だから…やったね…彼らはそのシステムを非同期にするために大きな変更を加えたんだ…そしてスレッドも使った。奇妙なことに、彼らはGILを聞いたことがなかったみたいで、俺がそれを説明したときの彼らの無表情が印象的だった。関係なかったけど。スレッドはいいものだよ。それから、彼らのテストがコードが壊れても常に合格していることを指摘したら、また無表情だった。彼らは、マグナムがすべてのバックグラウンドタスクや非同期のものをHTTPリクエストの最後に終わらせることを知らなかったから、応答を速めるための努力が無駄になってしまったんだ。知識があっても、他の人にそれを理解させることができなければ意味がない。

それは共感できるね。非同期やマルチスレッドコードのすべての可能なインタリーブをテストするのは、悪名高く難しい。高度なファジングツールや同時実行テストフレームワークを使っても、苦痛のある本番からの学びなしには完全な自信を得ることはめったにない。分散システムでは、さらに悪化するよ。例えば、Webhook配信インフラを設計する際には、サービス内の非同期コードだけでなく、ネットワークのリトライやタイムアウト、システム間の部分的な失敗にも対処しなければならない。信頼性の高いWebhookパイプラインを構築する際にこれに直面したことがある。リトライ、重複排除、高い同時実行性の下での冪等性を確保することが、完全なエンジニアリングの問題になったんだ。だから、多くのチームは今、Vartiq.comのような専門サービスにこれをオフロードしている。ここで働いているけど、これは自動リトライと可観測性を備えた保証されたWebhook配信を扱っている。これによって、自分のコード内の非同期テストの問題は解消されないけど、運用の同時実行性の複雑さを一部抽象化することで影響範囲を減らしている。完全に同意するよ – 非同期、スレッド、分散同時実行はお互いのリスクを増幅させる。コミュニケーションとシステム設計の注意が、どんな構文やライブラリの選択よりも重要だね。

Zigでは、テスト用のIo実装を用意する予定で、これはファジングを使って並行実行モデルの下でコードをストレステストする可能性があるよ。とはいえ、重要な洞察は、ほとんどのライブラリコードがio.asyncio.asyncConcurrentを呼び出さないと期待していることだ。例えば、ほとんどのデータベースライブラリはこれを必要とせず、シンプルな同期コードを含むことになる。でも、そのコードはアプリケーション開発者がより高いレベルで非同期性を表現するために使えるようになる:io.async(writeToDb)io.async(doOtherThing)。これによって、非同期/awaitがあちこちに散らばっているよりも、エラーが起こりにくく、理解しやすくなるよ。

この定義はあまり正しくないと思う: >非同期性:タスクが順序を無視して実行されても正しい可能性。これ、いいね。素晴らしい追加だし、確かに欠けてた。 >同時実行性:システムが複数のタスクを同時に進行させる能力、並列性やタスクスイッチを通じて。ここでは、マルチプロセッシングやタスクスイッチと言えるかな。 >並列性:システムが物理レベルで複数のタスクを同時に実行する能力。これは上で述べたように、技術的にはマルチプロセッシングだ。じゃあ、並列性と同時実行性の違いは何か?並列タスクはシェーダーのようなもので、同じタスクが物理レイヤーで同時に多くのインスタンスを実行している。GPUデバイスは並列計算が可能だよ。対して、同時実行タスクは物理レイヤーで同時に実行される異なるタスクだ。データも異なることが多い。例えば、スプライトエンジンが物理レイヤーでビデオディスプレイドライバーと同時に動いている場合。シェーダーは同じコードを実行しているけど、異なるデータ要素を処理している。例えば、各ピクセルが位置を持ち、より大きなレンダリングの一部になっている。GPUは大規模な並列マルチプロセッサだ。Threadripperは大規模な同時実行マルチプロセッサで、控えめな並列マルチプロセッサとしても機能する。違いは、さまざまな計算ユニットが何をできるか、そして実際に何をしているかにある。別の言い方をすれば、10GHzのシングルコアCPUはマルチプロセッサではない。これは逐次計算を行い、低いクロックレートのマルチプロセッサが処理できる同じタスク負荷を扱うためにタスクスイッチを行うことができる。10GHzのマルチコアCPUは同時実行マルチプロセッサだけど、GPUではない。これは、低いクロックのGPUと同等にシェーダーを実行できる。しかし、低いクロックのGPUは同じようにさまざまなタスクを実行することはできない。

パラレルタスクはシェーダーみたいなもんだよ。同じタスクを物理レイヤーで同時にたくさん実行してる感じ。それが「シングルインストラクションマルチプルデータ」ってやつで、これは別の問題だと思う。パラレル処理のいい例はFPGAだね。全てのゲートが同時に切り替わってるから、実際に全体を同期させる方法を考えないと、何も役に立たないものができないよ。* PLLは別として。

「非同期」という用語が必要かどうかを判断するいい方法は、それが単一の言語や単一の並行性デザイン以外の文脈でも役立つかどうかを試すことだね。もし広範な並行性モデルで正しく考えるために必要なら、役立つ追加になると思う。そうじゃなければ、全体的な観点から見ると使う価値はあまりないかな。つまり、Haskell、Erlang、OCaml、Scheme、Rust、Goなどで意味があるのか?(Haskell、Rust、OCamlの中からいくつかの並行性モデルを選んだ場合ね)。もっと一般的に言うと、協調的にスケジュールされている場合は、追加の詳細に注意を払う必要がある。悪いコードがシステム全体に影響を与えるのが簡単だから、ロックしたり遅延問題を引き起こしたりすることがあるからね。プリエンプティブにスケジュールされている世界では、大きな問題のグループが瞬時に消える。なぜなら、システムを同じようにロックできないから。