Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

並行プログラミング

この章の目的は、async の並行性がどのように機能し、スレッドによる並行性とどのように異なるのかについて、大まかなイメージを持ってもらうことです。実践的な詳細に入る前に、何が起きているのかについて適切なメンタルモデルを持つことが重要だと私は考えていますが、まず実際のコードを見たいタイプの人であれば、次の 1、2 章を読んでからこの章に戻ってくるのもよいでしょう。

まず動機から始め、次に逐次プログラミングスレッドまたはプロセスを使ったプログラミング、そしてasync プログラミングを扱います。この章は、並行性と並列性に関するセクションで終わります。

ユーザーは、コンピューターに複数のことをさせたいと考えます。ときには、ユーザーはそれらのことを同時に行いたいと考えます(たとえば、エディターで入力しながら同時に音楽アプリを聴くなど)。ときには、複数のタスクを同時に行うほうが効率的です(たとえば、大きなファイルをダウンロードしている間にエディターで作業を進めるなど)。ときには、複数のユーザーが同時に 1 台のコンピューターを使いたい場合もあります(たとえば、複数のクライアントがサーバーに接続している場合など)。

より低レベルの例を挙げると、音楽プログラムは、ユーザーがユーザーインターフェイス(UI)を操作している間も音楽を再生し続ける必要があるかもしれません。「音楽を再生し続ける」ためには、サーバーから音楽データをストリーミングし、そのデータをある形式から別の形式へ処理し、処理済みのデータをオペレーティングシステム(OS)経由でコンピューターのオーディオシステムに送る必要があるかもしれません。ユーザーに対しては、ユーザーの指示に応じてサーバーにデータやコマンドを送受信する必要があるかもしれませんし、音楽を再生しているサブシステムにシグナルを送る必要があるかもしれません(たとえば、ユーザーがトラックを変更したり一時停止したりした場合)。また、グラフィカル表示を更新する必要があるかもしれません(たとえば、ボタンを強調表示したりトラック名を変更したりするなど)。そして、上記すべてを行っている間も、マウスカーソルやテキスト入力の応答性を保たなければなりません。

複数のことを同時に行うこと(またはそう見えること)は、並行性と呼ばれます。プログラムは(OS と連携して)自身の並行性を管理しなければならず、その方法は数多くあります。この章ではそのいくつかを説明しますが、まずは完全に逐次的なコード、つまり並行性がまったくないコードから始めます。

逐次実行

ほとんどのプログラミング言語(Rust を含む)におけるデフォルトの実行モードは、逐次実行です。

do_a_thing();
println!("hello!");
do_another_thing();

各文は、次の文が開始される前に完了します1。それらの文の間では何も起こりません2。これは些細なことに聞こえるかもしれませんが、コードについて推論するうえで非常に有用な性質です。しかし、それは同時に、多くの時間を無駄にしていることも意味します。上の例では、println!("hello!") が起こるのを待っている間に、do_another_thing() を実行できたかもしれません。場合によっては、3 つの文すべてを同時に実行できたかもしれません。

IO3 が発生するときはいつでも(println! を使って出力することは IO です。OS への呼び出しを通じてコンソールにテキストを出力しているためです)、プログラムは次の文を実行する前に IO が完了する4のを待ちます。実行を続ける前に IO の完了を待つことは、プログラムが他の進行を行うのをブロックします。ブロッキング IO は、利用、実装、推論が最も簡単な種類の IO ですが、同時に最も効率の低いものでもあります。逐次的な世界では、IO の完了を待っている間、プログラムは何もできません。

プロセスとスレッド

プロセスとスレッドは、並行性を提供するためにオペレーティングシステムによって提供される概念です。実行可能ファイルごとに 1 つのプロセスがあるため、複数のプロセスをサポートするということは、コンピューターが複数のプログラム5を並行して実行できるということを意味します。1 つのプロセスには複数のスレッドを持たせることができるため、プロセスの内部にも並行性が存在できます。

プロセスとスレッドの扱われ方には、多くの小さな違いがあります。最も重要な違いは、メモリはスレッド間では共有されますが、プロセス間では共有されない6という点です。つまり、プロセス間の通信は、何らかのメッセージパッシングによって行われます。これは、別々のコンピューターで実行されているプログラム間で通信するのに似ています。プログラムの観点では、単一のプロセスがその世界全体です。新しいプロセスを作成することは、新しいプログラムを実行することを意味します。一方で、新しいスレッドを作成することは、単にプログラムの通常の実行の一部です。

プロセスとスレッドのこうした違いのため、プログラマーにとってそれらは非常に異なるものに感じられます。しかし OS の観点では、それらは非常によく似ており、ここでは単一の概念であるかのようにその性質を説明します。ここではスレッドについて話しますが、特に断らない限り、それは「スレッドまたはプロセス」を意味すると理解してください。 OS はスレッドのスケジューリングを担当します。つまり、スレッドをいつ実行し、どのくらいの時間実行するかを決定します。最近のコンピューターの多くは複数のコアを備えているため、文字どおり同時に複数のスレッドを実行できます。しかし、コア数よりもはるかに多くのスレッドが存在することは一般的なので、OS は各スレッドを短い時間だけ実行し、その後それを一時停止して、別のスレッドをしばらく実行します7。複数のスレッドがこのような形で単一のコア上で実行される場合、それはインターリーブまたはタイムスライシングと呼ばれます。OS がスレッドの実行をいつ一時停止するかを選択するため、これはプリエンプティブマルチタスクと呼ばれます(ここでのマルチタスクとは、単に複数のスレッドを同時に実行することを意味します)。OS はスレッドの実行をプリエンプトします(より詳しく言えば、OS は実行をプリエンプティブに一時停止します。プリエンプティブである理由は、最初のスレッドが本来なら一時停止する前に、別のスレッドのための時間を作るために OS がそのスレッドを一時停止し、2 つ目のスレッドが実行できないことが問題になる前に実行できるようにするからです)。

IO についてもう一度見てみましょう。スレッドが IO を待ってブロックすると何が起こるでしょうか?スレッドを備えたシステムでは、OS はそのスレッドを一時停止し(いずれにせよ待つだけなので)、IO が完了したときに再び起こします8。スケジューリングアルゴリズムによっては、IO が完了してから OS が IO を待っているスレッドを起こすまでに少し時間がかかる場合があります。これは、OS が他のスレッドにいくらか作業を進めさせるために待つことがあるためです。これにより、物事ははるかに効率的になります。あるスレッドが IO を待っている間、別のスレッド(またはマルチタスクにより、より可能性が高いのは多数のスレッド)が進捗できます。しかし、IO を行っているスレッドの視点では、物事は依然として逐次的です。そのスレッドは次の操作を開始する前に IO が完了するのを待ちます。

スレッドは、通常はタイムアウト付きで sleep 関数を呼び出すことにより、自分自身を一時停止することも選択できます。この場合、OS はスレッド自身の要求によってスレッドを一時停止します。プリエンプションや IO による一時停止と同様に、OS は後で(タイムアウト後に)スレッドを再び起こして実行を継続させます。

OS が(何らかの理由で)あるスレッドを一時停止して別のスレッドを開始することは、コンテキストスイッチと呼ばれます。切り替えられるコンテキストには、レジスタ、オペレーティングシステムの記録、多くのキャッシュの内容が含まれます。これは決して些細な作業量ではありません。OS への制御の移行とスレッドへの復帰、そして古くなったキャッシュを扱うコストと合わせて、コンテキストスイッチはコストの高い操作です。

最後に、一部のハードウェアや OS はプロセスやスレッドをサポートしていないことに注意してください。これは組み込みの世界でより起こりやすいことです。

非同期プログラミング

非同期プログラミングは、スレッドによる並行処理と同じ高レベルの目標(同時に多くのことを行う)を持つ一種の並行処理ですが、実装は異なります。非同期の並行処理とスレッドによる並行処理の 2 つの大きな違いは、非同期の並行処理は OS の助けを借りずに完全にプログラム内で管理されること9、そしてマルチタスクがプリエンプティブではなく協調的であること10です(これについてはすぐに説明します)。非同期の並行処理には多くの異なるモデルがあります。これらについてはこのガイドの後半で比較しますが、今は Rust のモデルだけに焦点を当てます。

スレッドと区別するために、非同期の並行処理における一連の実行をタスクと呼ぶことにします(これらはグリーンスレッドとも呼ばれますが、これはプリエンプティブスケジューリングやタスクごとに 1 つのスタックを持つといった実装詳細の含意を伴うことがあります)。タスクが実行され、スケジュールされ、メモリ内で表現される方法はスレッドとは大きく異なりますが、高レベルの直感としては、タスクを、OS ではなく完全にプログラム内で管理されるスレッドのようなものと考えると役に立つことがあります。

非同期システムにも、次にどのタスクを実行するかを決定するスケジューラーは存在します(これはプログラムの一部であり、OS の一部ではありません)。しかし、スケジューラーはタスクをプリエンプトできません。代わりに、タスクは自発的に制御を手放し、別のタスクがスケジュールされることを許可しなければなりません。タスクは(制御を手放すことで)協調しなければならないため、これは協調的マルチタスクと呼ばれます。

プリエンプティブではなく協調的マルチタスクを使用することには、多くの影響があります。

  • 制御が譲られる可能性のある地点の間では、コードが逐次的に実行されることを保証できます。予期せず一時停止されることはありません。
  • タスクが yield ポイントの間で長い時間を費やす場合(たとえば、ブロッキング IO を行ったり、長時間実行される計算を実行したりする場合)、他のタスクは進捗できません。
  • スケジューラーの実装ははるかに単純であり、スケジューリング(およびコンテキストスイッチ)のオーバーヘッドは少なくなります。

非同期の並行処理は、スレッドによる並行処理よりもはるかに効率的です。メモリのオーバーヘッドははるかに低く、コンテキストスイッチはずっと低コストな操作です。OS へ制御を渡してプログラムへ戻す必要がなく、切り替えるデータもはるかに少ないからです。しかし、それでもキャッシュへの影響がいくらか生じることがあります。OS のキャッシュ、たとえば TLB は変更する必要がないものの、タスクはメモリの異なる部分を操作する可能性が高いため、新しくスケジュールされたタスクに必要なデータがメモリキャッシュにない場合があります。

非同期 IO はブロッキング IO の代替です(ノンブロッキング IO と呼ばれることもあります)。非同期 IO は非同期の並行処理と直接結び付いているわけではありませんが、この 2 つは一緒に使われることがよくあります。非同期 IO では、プログラムは 1 つのシステムコールで IO を開始し、その後 IO が完了したかどうかを確認するか、完了時に通知を受け取ることができます。つまり、IO が行われている間、プログラムは他の作業を自由に進められます。Rust では、非同期 IO の仕組みは非同期ランタイムによって処理されます(スケジューラーもランタイムの一部です。ランタイムについてはこの本の後半でより詳しく説明しますが、本質的には、ランタイムは基本的な非同期関連のことの一部を処理するライブラリにすぎません)。 システム全体の観点から見ると、スレッドを使った並行システムにおけるブロッキング IO と、async 並行システムにおけるノンブロッキング IO は似ています。どちらの場合も、IO には時間がかかり、IO が行われている間に他の作業が実行されます。

  • スレッドの場合、IO を行うスレッドは OS に IO を要求し、そのスレッドは OS によって一時停止され、他のスレッドが作業を実行します。そして IO が完了すると、OS はそのスレッドを起こし、IO の結果を使って実行を継続できるようにします。
  • async の場合、IO を行うタスクはランタイムに IO を要求し、ランタイムは OS に IO を要求しますが、OS はランタイムに制御を返します。ランタイムは IO タスクを一時停止し、他のタスクをスケジュールして作業を実行させます。IO が完了すると、ランタイムは IO タスクを起こし、IO の結果を使って実行を継続できるようにします。

async IO を使う利点は、オーバーヘッドがはるかに低いため、システムがスレッドよりも桁違いに多くのタスクをサポートできることです。そのため、async 並行性は、多数のユーザーを持ち、IO 待ちに多くの時間を費やすタスクに特に適しています(待ち時間がそれほど多くなく、代わりに CPU バウンドな作業を大量に行う場合は、ボトルネックが CPU とメモリリソースになるため、低オーバーヘッドであることによる利点はそれほど大きくありません)。

スレッドと async は相互排他的ではありません。多くのプログラムは両方を使用します。一部のプログラムには、スレッドを使って実装する方が適している部分と、async を使って実装する方が適している部分があります。たとえば、データベースサーバーはクライアントとのネットワーク通信を管理するために async の手法を使い、データに対する計算には OS スレッドを使うことがあります。あるいは、プログラムが async 並行性だけを使って書かれていても、ランタイムが複数のスレッド上でタスクを実行することがあります。これは、プログラムが複数の CPU コアを利用するために必要です。本書の後のいくつかの箇所で、スレッドと async タスクが交差する部分を扱います。

並行性と並列性

ここまでは並行性(同時に多くのことを行う、または行っているように見えること)について話してきました。また、並列性(文字どおり同時に多くのことを行うことを可能にする複数の CPU コアの存在)にも触れてきました。これらの用語は同じ意味で使われることもありますが、異なる概念です。このセクションでは、これらの用語とその違いを正確に定義してみます。説明のために簡単な疑似コードを使います。

1 つのタスクが多数のサブタスクに分割されていると想像してください。

task1 {
  subTask1-1()
  subTask1-2()
  ...
  subTask1-100()
}

このような疑似コードを実行するプロセッサのふりをしてみましょう。実行する明らかな方法は、まず subTask1-1 を実行し、次に subTask1-2 を実行し、すべてのサブタスクが完了するまで同じように続けることです。これは逐次実行です。

次に、複数のタスクを考えてみましょう。それらをどのように実行できるでしょうか。1 つのタスクを開始し、タスク全体が完了するまですべてのサブタスクを実行してから、次のタスクに取りかかることができます。2 つのタスクは逐次的に実行されています(また、各タスク内のサブタスクも逐次的に実行されています)。サブタスクだけを見ると、次のように実行することになります。

subTask1-1()
subTask1-2()
...
subTask1-100()
subTask2-1()
subTask2-2()
...
subTask2-100()

あるいは、subTask1 を実行してから task1 を脇に置き(どこまで進んだかを覚えておき)、次のタスクを取り上げてその最初のサブタスクを実行し、その後 task1 に戻ってサブタスクを実行することもできます。2 つのタスクはインターリーブされており、これを 2 つのタスクの並行実行と呼びます。次のようになります。

subTask1-1()
subTask2-1()
subTask1-2()
subTask2-2()
...
subTask1-100()
subTask2-100()

あるタスクが別のタスクの結果や副作用を観測できない限り、そのタスクの観点からは、サブタスクは依然として逐次的に実行されています。

2 つのタスクに限定する理由はありません。任意の数のタスクを、任意の順序でインターリーブできます。

どれだけ並行性を追加しても、処理全体の完了にかかる時間は同じであることに注意してください(実際には、それらの間でコンテキストスイッチを行うオーバーヘッドにより、並行性が増えるほど時間が長くなる可能性があります)。ただし、ある特定のサブタスクについては、完全に逐次的に実行する場合よりも早く完了するかもしれません(ユーザーにとっては、より応答性が高いように感じられるかもしれません)。

次に、タスクを処理しているのが自分だけではなく、手伝ってくれるプロセッサの仲間がいると想像してください。同時にタスクに取り組み、作業をより速く完了できます! これが並列実行です(これは並行でもあります)。サブタスクは次のように実行されるかもしれません。

Processor 1           Processor 2
==============        ==============
subTask1-1()          subTask2-1()
subTask1-2()          subTask2-2()
...                   ...
subTask1-100()        subTask2-100()

プロセッサが 2 つより多い場合は、さらに多くのタスクを並列に処理できます。また、各プロセッサ上でタスクをインターリーブしたり、プロセッサ間でタスクを共有したりすることもできます。

実際のコードでは、物事は少し複雑です。一部のサブタスク(たとえば IO)は、プロセッサが能動的に関与する必要はなく、開始して、しばらく後に結果を回収するだけで済みます。また、一部のサブタスクは、進捗するために別のタスクのサブタスクの結果(または副作用)を必要とする場合があります(同期)。これら 2 つのシナリオはいずれも、タスクを並行に実行できる有効な方法を制限します。そして、それに何らかの公平性の概念を確保することが加わるため、スケジューリングが重要になります。

ふざけた例はもう十分なので、きちんと定義してみましょう

並行性は計算の順序に関するものであり、並列性は実行の形態に関するものです。

2 つの計算があるとき、一方が他方より前に起こることを観測できる場合、それらは逐次的(つまり並行ではない)であると言います。一方が他方より前に起こることを観測できない(または、それが重要でない)場合、それらは並行であると言います。

2 つの計算は、文字どおり同時に起こっている場合に並列に起こっています。並列性はリソースとして考えることができます。利用可能な並列性が多いほど、一定の時間内により多くの計算を行うことができます(計算が同じ速度で行われると仮定した場合)。並列性を増やさずにシステムの並行性を高めても、それによってシステムが速くなることは決してありません(ただし、システムの応答性を高めることはでき、そうでなければ実用的でない最適化を実装可能にすることはあります)。 言い換えると、2つの計算は一方が他方の後に起こる場合(並行でも並列でもない)もあれば、それらの実行が単一のCPUコア上でインターリーブされる場合(並行だが並列ではない)もあり、あるいは2つのコア上で同時に実行される場合(並行かつ並列)もあります11

もう1つ有用な捉え方12は、並行性はコードを構成する方法であり、並列性はリソースである、というものです。これは強力な主張です!並行性がコードの実行ではなくコードの構成に関するものだという点は重要です。なぜなら、プロセッサの観点から見ると、並列性を伴わない並行性は単に存在しないからです。これは async の並行性にとって特に重要です。なぜなら、それは完全にユーザー側のコードで実装されているからです。つまり、それはコードを構成すること「だけ」に関するものだというだけでなく、ソースコードを読むだけでそれを簡単に自分で確かめられます。並列性がリソースであるという点も有用です。なぜなら、並列性と性能にとって重要なのはプロセッサコアの数だけであり、並行性に関してコードがどのように構成されているか(たとえばスレッドがいくつあるか)ではない、ということを思い出させてくれるからです。

スレッドベースのシステムと async システムはどちらも、並行性と並列性の両方を提供できます。どちらの場合も、並行性はコード(スレッドまたはタスクの生成)によって制御され、並列性はスケジューラによって制御されます。スケジューラは、スレッドではOSの一部(OSのAPIによって設定される)であり、async ではランタイムライブラリの一部(ランタイムの選択、ランタイムの実装方法、およびランタイムがクライアントコードに提供するオプションによって設定される)です。ただし、慣習と一般的なデフォルトに起因する実践上の違いがあります。スレッドベースのシステムでは、各並行スレッドは、可能な限り多くの並列性を使って並列に実行されます。async システムには強いデフォルトはありません。すべてのタスクを単一のスレッドで実行するシステムもあれば、複数のタスクを単一のスレッドに割り当て、そのスレッドを1つのコアに固定するシステムもあります(そのため、タスクのグループは並列に実行されますが、グループ内では各タスクは並行に実行され、同じグループ内の他のタスクと並列に実行されることはありません)。また、タスクが制限付きまたは制限なしで並列に実行される場合もあります。このガイドの第1部では、主に最後のモデルをサポートする Tokio ランタイムを使用します。つまり、並列性に関する振る舞いは、スレッドを使った並行性の場合と似ています。さらに、async Rust には、ランタイムに依存せず、並行性は明示的にサポートするが並列性はサポートしない機能があることも見ていきます。

まとめ

  • 実行モデルは数多くあります。ここでは、逐次実行、スレッドとプロセス、非同期プログラミングについて説明しました。
    • スレッドは、OSによって提供され(かつスケジュールされ)る抽象化です。通常、プリエンプティブマルチタスクを伴い、デフォルトで並列であり、管理とコンテキストスイッチングにはかなり高いオーバーヘッドがあります。
    • 非同期プログラミングは、ユーザー空間のランタイムによって管理されます。マルチタスクは協調的です。スレッドよりもオーバーヘッドは低いですが、第一級のスレッドではなく、異なるプログラミングプリミティブ(asyncawait、および Future)を使用するため、スレッドを使ったプログラミングとは少し感覚が異なります。
  • 並行性と並列性は異なる概念ですが、密接に関連しています。
    • 並行性は計算の順序に関するものです(操作の実行順序を観測できない場合、それらの操作は並行です)。
    • 並列性は複数のプロセッサ上で計算することに関するものです(操作が文字どおり同時に起こっている場合、それらの操作は並列です)。
  • OSスレッドと非同期プログラミングはいずれも、並行性と並列性を提供します。非同期プログラミングはさらに、多くのオペレーティングシステムのスレッドAPIには含まれない、柔軟または細粒度の並行性のための構成要素も提供できます。

  1. これは厳密には正しくありません。現代のコンパイラーや CPU はコードを再構成し、好きな順序で実行します。逐次的な文は、多くの異なる方法で重なり合っている可能性があります。しかし、これはプログラム自身やそのユーザーから決して観測可能であってはなりません。

  2. これも正しくありません。あるプログラムが完全に逐次的であっても、他のプログラムが同時に実行されている可能性があります。これについては次のセクションで詳しく説明します。

  3. IO は input/output の略です。これは、プログラムからプログラム外部の世界へのあらゆる通信を意味します。ディスクやネットワークへの読み書き、端末への書き込み、キーボードやマウスからのユーザー入力の取得、OS やシステム内で実行されている別のプログラムとの通信などが含まれます。並行性の文脈で IO が興味深いのは、プログラムが内部で行うほぼあらゆるタスクよりも、発生するまでに何桁も長い時間がかかるためです。これは通常、多くの待ち時間を意味し、その待ち時間は他の作業を行う機会になります。

  4. IO が正確にいつ完了するかは、実際にはかなり複雑です。プログラムの観点では、1 回の IO 呼び出しは、OS から制御が戻されたときに完了します。これは通常、データが何らかのハードウェアや別のプログラムに送信されたことを示しますが、そのデータが実際にディスクへ書き込まれた、またはユーザーに表示された、などを必ずしも意味するわけではありません。それには、ハードウェアでの追加作業、キャッシュの定期的なフラッシュ、あるいは別のプログラムがデータを読み取ることが必要な場合があります。ほとんどの場合、これについて心配する必要はありませんが、知っておくとよいでしょう。

  5. ユーザーの視点では、単一のプログラムに複数のプロセスが含まれる場合がありますが、OS の視点では各プロセスは別個のプログラムです。

  6. 一部の OS はプロセス間でのメモリ共有をサポートしていますが、それを使うには特別な扱いが必要で、ほとんどのメモリは共有されません。

  7. OS がどのスレッドをどのくらいの時間(そしてどのコアで)実行するかを正確にどのように選ぶかは、スケジューリングの重要な部分です。高レベルの戦略と、それらの戦略を設定するためのオプションの両方に、多くの選択肢があります。ここで適切な選択を行うことは良好なパフォーマンスにとって非常に重要ですが、複雑であり、ここでは掘り下げません。

  8. もう 1 つの選択肢として、スレッドが IO の終了まで単にループで回り続けることでビジーウェイトすることがあります。これは、他のスレッドが実行できなくなるためあまり効率的ではなく、ほとんどの最近のシステムでは一般的ではありません。ロックの実装や非常に単純な組み込みシステムで見かけることがあるかもしれません。

  9. 最初は、プログラムに単一のスレッドしかないと仮定して説明を始めますが、後でそれを拡張します。システム上ではおそらく他のプロセスも実行されていますが、それらは async 並行性の仕組みに実際には影響しません。

  10. プログラム内で(OS を使わずに)管理される並行性を持ちながら、スレッド間の協調に依存するのではなくプリエンプティブスケジューラーを使うプログラミング言語(あるいはライブラリさえ)もいくつかあります。Go はよく知られた例です。これらのシステムでは asyncawait の表記は必要ありませんが、他の言語や OS との相互運用がはるかに難しくなることや、ランタイムが重量級になることなど、別の欠点があります。Rust のごく初期のバージョンにはそのようなシステムがありましたが、1.0 までにはその痕跡は残っていませんでした。

  11. 計算は並列だが並行ではない、ということはあり得るのでしょうか?ある意味ではそうですが、実際にはそうとは言えません。2つのタスク(aとb)があり、それぞれが1つのサブタスク(それぞれaとbに属する1と2)で構成されていると想像してください。同期を使うことで、サブタスク1が完了するまでサブタスク2を開始できず、タスクaはサブタスク2が完了するまで完了できないものとします。ここで、aとbは異なるプロセッサ上で実行されます。タスクをブラックボックスとして見るなら、それらは並列に実行されていると言えますが、ある意味では、それらの順序は完全に決定されているため、並行ではありません。しかし、サブタスクを見ると、それらは並列でも並行でもないことがわかります。

  12. これはAaron Turonによるものだと思っており、Rustの標準ライブラリの設計の一部にも反映されています。たとえば、available_parallelism 関数です。