ホームページ  >  記事  >  バックエンド開発  >  Go はなぜ「速い」のでしょうか?

Go はなぜ「速い」のでしょうか?

藏色散人
藏色散人転載
2020-03-04 09:34:312844ブラウズ

この記事では主に、非常に高い同時実行パフォーマンスを達成するための Go プログラムの内部スケジューラー実装アーキテクチャ (G-P-M モデル) と、コンピューティング リソースの使用を最大化するために Go スケジューラーがスレッド ブロッキング シナリオを処理する方法を紹介します。

システムを高速化する方法

情報技術の急速な発展に伴い、単一サーバーの処理能力はますます強化されており、プログラミング モデルは以前のシリーズからの変更点 Row モードがコンカレント モデルにアップグレードされました。

同時実行モデルには、IO 多重化、マルチプロセス、マルチスレッドが含まれます。これらの各モデルには、独自の長所と短所があります。最新の複雑な高同時実行アーキテクチャのほとんどは、複数のモデルを一緒に使用し、異なるモデルが使用されます。さまざまなシナリオで、長所を利用し、短所を回避して、サーバーのパフォーマンスを最大化します。

マルチスレッドは軽量で使いやすいため、同時プログラミングで最も頻繁に使用される同時実行モデルとなっています。これには、後から派生したコルーチンやその他のサブ製品も含まれます。これらもそれに基づいています。

同時実行 ≠ 並列

同時実行と並列処理は異なります。

単一の CPU コア上で、スレッドは複数のタスクを「同時に」実行するという目的を達成するために、タイム スライスによるタスクの切り替えや制御権の放棄を実現します。これがいわゆる同時実行性です。しかし実際には、常に 1 つのタスクのみが実行され、他のタスクは何らかのアルゴリズムを通じてキューに入れられます。

マルチコアCPUでは、同じプロセス内の「複数のスレッド」を本当の意味で同時に実行することができ、これが並列処理です。

プロセス、スレッド、コルーチン

プロセス: プロセスは、システム内のリソース割り当ての基本単位であり、独立したメモリ空間を持っています。

スレッド: スレッドは CPU のスケジューリングとディスパッチの基本単位です。スレッドはプロセスに接続され、各スレッドは親プロセスのリソースを共有します。

コルーチン: コルーチンはユーザー モードの軽量スレッドです。コルーチンのスケジュールはユーザーによって完全に制御されます。コルーチン間の切り替えには、カーネルのオーバーヘッドなしで、タスクのコンテキストを保存するだけで済みます。

スレッド コンテキストの切り替え

割り込み処理、マルチタスク、ユーザー モードの切り替えなどの理由により、CPU はあるスレッドから別のスレッドに切り替わり、切り替えプロセスでは現在のプロセスの状態を保存し、別のプロセスの状態を復元します。

コンテキストスイッチングは、コア上のスレッドを交換するのに時間がかかるため、コストがかかります。コンテキスト スイッチの遅延はさまざまな要因に依存し、50 ~ 100 ナノ秒の範囲になります。ハードウェアがコアごとにナノ秒あたり平均 12 命令を実行することを考慮すると、コンテキスト スイッチの遅延は 600 ~ 1200 命令かかる可能性があります。実際、コンテキストの切り替えは、プログラムの命令実行に多くの時間を費やします。

クロスコア コンテキスト スイッチがある場合、CPU キャッシュ障害が発生する可能性があります (キャッシュからデータにアクセスするための CPU のコストは約 3 ~ 40 クロック サイクルであり、キャッシュからデータにアクセスするためのコストは約 3 ~ 40 クロック サイクルです)。メイン メモリの速度は約 100 ~ 300 クロック サイクルです)、このシナリオでのスイッチング コストはより高価になります。

Golang は同時実行のために生まれました

Golang は、2009 年の正式リリース以来、その極めて高い実行速度と効率的な開発効率を利用して、すぐに市場シェアを獲得してきました。 Golang は言語レベルで同時実行をサポートしており、軽量のコルーチン Goroutine を使用してプログラムの同時実行を実現します。

Goroutine は非常に軽量であり、主に次の 2 つの側面に反映されています:

コンテキスト切り替えのコストが小さい: Goroutine のコンテキスト切り替えには 3 つのレジスタ (PC / SP / DX) の値の変更のみが含まれます。 ); 対照的に、スレッドのコンテキスト切り替えには、モード切り替え (ユーザー モードからカーネル モードへの切り替え) と 16 個のレジスタ、PC、SP...、その他のレジスタのリフレッシュが必要です;

低メモリ使用量: スレッド スタックスペースは通常 2M、最小 Goroutine スタック スペースは 2K;

Golang プログラムは 10w レベルの Goroutine 操作を簡単にサポートでき、スレッド数が 1k に達すると、メモリ使用量は 2G に達します。

Go スケジューラの実装メカニズム:

Go プログラムは、スケジューラを使用して Goroutine がカーネル スレッドで実行されるようにスケジュールしますが、Goroutine は OS スレッド M に直接バインドされていません。 -Machine Goroutine Scheduler の P-Processor (論理プロセッサ) は、実行する代わりに、カーネル スレッド リソースを取得するための「仲介者」として機能します。

Go スケジューラ モデルは通常、G-P-M モデルと呼ばれます。これには、G、P、M、および Sched という 4 つの重要な構造が含まれています:

G: Goroutine。各 Goroutine は G 構造に対応します。 Body、G には、Goroutine の実行中のスタック、ステータス、タスク関数が保存され、再利用できます。

G は実行本体ではありません。実行をスケジュールするには、各 G を P にバインドする必要があります。

P: プロセッサ。論理プロセッサを表します。G の場合、P は CPU コアに相当します。G は、P にバインドされている場合にのみスケジュールできます。 M の場合、P はメモリ割り当てステータス (mcache)、タスク キュー (G) などの関連する実行環境 (Context) を提供します。

P の数によって、システム内で並列化できる G の最大数が決まります (前提: 物理 CPU コアの数 >= P の数)。

P の数はユーザーが設定した GoMAXPROCS によって決まりますが、GoMAXPROCS 設定がどれほど大きくても、P の最大数は 256 です。

M: マシン (OS カーネル スレッドの抽象化) は、実際に計算を実行するリソースを表します。有効な P をバインドした後、スケジュール ループに入ります。スケジュール ループのメカニズムは、大まかに、グローバル キュー、P のローカル キュー、待機から構成されます。キューから取得。

M の数は可変で、Go ランタイムによって調整されます。作成されすぎてシステムが多数の OS スレッドをスケジュールしないようにするために、現在のデフォルトの最大制限は 10,000 です。

M は G の状態を保持しません。これは、G が M 全体でスケジュールされる基礎となります。

Sched: Go スケジューラ。M と G およびスケジューラの一部のステータス情報を保存するキューを維持します。

スケジューラのサイクルの仕組みは、大まかに言うと、さまざまなキューやPのローカルキューからGを取得し、Gの実行スタックに切り替えてGの関数を実行し、Goexitを呼び出してクリーンアップ作業を行ってMに戻る、という繰り返しです。

M、P、G の関係を理解するには、レンガを移動させるホリネズミ カートの古典的なモデルを通じて関係を説明できます。

Go はなぜ「速い」のでしょうか?

ゴーファーの仕事の内容は、建設現場にはたくさんのレンガがあり、ゴーファーはトロリーを使ってレンガを焼くために火のところまで運ぶというものです。 M は写真のホリネズミ、P は車、G は車に取り付けられたレンガとみなすことができます。

3 人の関係を明らかにした後、ホリネズミがどのようにレンガを運ぶかに焦点を当ててみましょう。

プロセッサ (P):

ユーザーが設定した GoMAXPROCS 値に基づいて車のバッチ (P) を作成します。

Goroutine(G):

Go キーワードは、Goroutine を作成するために使用されます。これは、ブリック (G) を作成し、このブリック (G) を現在の This に配置するのと同じです。車は(P)にあります。

マシン (M):

モル (M) を外部から作成できません。レンガ (G) が多すぎ、モル (M) が少なすぎます。とても忙しいです。しかし、たまたま使用されていない空き車 (P) があった場合は、すべての車 (P) が使い果たされるまで、他の場所からさらにゴーファー (M) を借ります。

モール(M)だけでは足りず、モール(M)を他から借りてカーネルスレッド(M)を作成する処理があります。

ゴーファー (M) はカート (P) なしではレンガを運ぶことができないことに注意してください。カート (P) の数によって、作業できるゴーファー (M) の数が決まります。プログラムはアクティブなスレッドの数です;

Go プログラムでは、次の図を使用して G-P-M モデルを表示します:

Go はなぜ「速い」のでしょうか?

P は「並列」を表します。 「論理プロセッサが実行され、各 P はシステム スレッド M に割り当てられ、G は Go コルーチンを表します。

Go スケジューラには、グローバル実行キュー (GRQ) とローカル実行キュー (LRQ) という 2 つの異なる実行キューがあります。

各 P には LRQ があり、P のコンテキストで実行するように割り当てられたゴルーチンを管理するために使用されます。これらのゴルーチンは、P にバインドされた M によってコンテキストが切り替えられます。 GRQ は、まだ P に割り当てられていないゴルーチンに適用されます。

上の図からわかるように、G の数は M の数よりもはるかに大きくなる可能性があります。言い換えると、Go プログラムは少数のカーネルレベルのスレッドを使用して同時実行性をサポートできます。多数のゴルーチンの。複数の Goroutine は、ユーザーレベルのコンテキスト切り替えを通じてカーネル スレッド M のコンピューティング リソースを共有しますが、オペレーティング システムのスレッド コンテキスト切り替えによって引き起こされるパフォーマンスの損失はありません。

スレッド コンピューティング リソースを最大限に活用するために、Go スケジューラは次のスケジューリング戦略を採用します。

タスク スティーリング (ワーク スティーリング)

私たちは、現実を知っています。 Go では、フィッシング P の存在を絶対に許可せず、コンピューティング リソースを最大限に活用する必要があります。 。

Go の並列処理能力を向上させ、全体の処理効率を高めるために、各 P 間の G タスクがアンバランスな場合、スケジューラは G の実行を他の P の GRQ または LRQ から取得できるようにします。

ブロッキングを減らす

実行中の Goroutine がスレッド M をブロックしたらどうなるでしょうか? P上のLRQのGoroutineはスケジューリングを取得できなくなるのでしょうか?

Go でのブロックは主に次の 4 つのシナリオに分かれています:

シナリオ 1: Goroutine はアトミック、ミューテックス、またはチャネル操作呼び出しによりブロックされ、スケジューラは現在ブロックされている Goroutine Go を切り替えます。 LRQ 上の他の Goroutine をアウトして再スケジュールする;

シナリオ 2: Goroutine はネットワーク要求と IO 操作によりブロックされます。このブロックの場合、G と M は何をしますか?

Go プログラムは、ネットワーク リクエストと IO 操作を処理するネットワーク ポーラー (NetPoller) を提供します。そのバックグラウンドでは、kqueue (MacOS)、epoll (Linux)、または iocp (Windows) を使用して IO 多重化を実装します。

NetPoller を使用してネットワーク システム コールを行うことにより、スケジューラは、これらのシステム コールを行うときに Goroutine が M をブロックするのを防ぐことができます。これにより、M は新しい M を作成せずに P の LRQ で他のゴルーチンを実行できるようになります。オペレーティング システムのスケジュール負荷を軽減します。

次の図は、その仕組みを示しています。G1 は M 上で実行されており、LRQ 上で実行を待機している 3 つのゴルーチンがあります。ネットワーク ポーラーはアイドル状態で、何も行いません。

Go はなぜ「速い」のでしょうか?

次に、G1 はネットワーク システム コールを実行したいため、ネットワーク ポーラーに移動して、非同期ネットワーク システム コールを処理します。その後、M は LRQ から追加のゴルーチンを実行できます。このとき、G2 は M にコンテキスト スイッチされます。

Go はなぜ「速い」のでしょうか?

最後に、非同期ネットワーク システム コールはネットワーク ポーラーによって完了し、G1 は P の LRQ に戻されます。 G1 が M でコンテキストを切り替えることができると、G1 が担当する Go 関連のコードを再度実行できます。ここでの大きな利点は、ネットワーク システム コールを実行するために追加の M が必要ないことです。ネットワーク ポーラーはシステム スレッドを使用し、常にアクティブなイベント ループを処理します。

Go はなぜ「速い」のでしょうか?

この呼び出しメソッドは非常に複雑に見えます。幸いなことに、Go 言語はランタイムにこの「複雑さ」を隠します。Go 開発者は、ソケットがソケットであるかどうかに注意を払う必要はありません。ノンブロックなので、ファイルディスクリプタのコールバックを独自に登録する必要がなく、各コネクションに対応したゴルーチン内の「ブロックI/O」メソッドでソケットを扱うだけで済み、シンプルなゴルーチンあたりの処理を実現します。 -接続ネットワーク プログラミング モード (ただし、Goroutine の数が多いと、スタック メモリの増加やスケジューラの負担の増加など、追加の問題も発生します)。

ユーザー層から見えるGoroutineの「ブロックソケット」は、実際にはGoランタイムのネットポーラーを介した非ブロックソケットI/O多重化メカニズムによって「シミュレート」されます。 Go のネット ライブラリはまさにこの方法で実装されます。

シナリオ 3: 一部のシステム メソッドを呼び出すときに、システム メソッドがブロックされている場合、この場合、ネットワーク ポーラー (NetPoller) は使用できず、システム コールを行うゴルーチンは現在の M をブロックします。

同期システム コール (ファイル I/O など) によって M がブロックされる状況を見てみましょう。G1 は同期システム コールを実行して M1 をブロックします。

Go はなぜ「速い」のでしょうか?

スケジューラが介入すると、G1 が M1 をブロックしたことを認識し、この時点でスケジューラは M1 を P から分離し、G1 も奪います。次に、スケジューラは P にサービスを提供する新しい M2 を導入します。この時点で、LRQ から G2 を選択でき、M2 でコンテキスト スイッチを実行できます。

Go はなぜ「速い」のでしょうか?

ブロックされたシステム コールが完了したら、G1 を LRQ に戻し、P によって再度実行できます。このようなことが再び起こった場合、M1 は将来の再利用のために取っておかれます。

Go はなぜ「速い」のでしょうか?

シナリオ 4: Goroutine でスリープ操作が実行されると、M はブロックされます。

Go プログラムのバックグラウンドには監視スレッド sysmon があり、長時間実行される G タスクを監視し、他の Goroutine がそれらを先制して実行できるように、横取りできる識別子を設定します。

このゴルーチンが次回関数呼び出しを行う限り、ゴルーチンは占有され、シーンも保護され、その後 P のローカル キューに戻されて次の実行を待ちます。

概要

この記事では主に、Go スケジューラ アーキテクチャの観点から G-P-M モデルを紹介します。このモデルを通じて、サポートするカーネル スレッドの数を減らす方法について説明します。多数のゴルーチンの同時実行。また、NetPoller、sysmon などを通じて、Go プログラムがスレッド ブロッキングを軽減し、既存のコンピューティング リソースを最大限に活用できるようにすることで、Go プログラムの動作効率を最大化します。

go 言語の知識について詳しくは、php 中国語 Web サイトの go 言語チュートリアル 列に注目してください。

以上がGo はなぜ「速い」のでしょうか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事は腾讯技术工程で複製されています。侵害がある場合は、admin@php.cn までご連絡ください。