Golangのスケジューリングモデル--GMP

GMPとは何ですか?

これは、golang自体によって実装されるスケジューラーであり、「G」、「M」、および「P」によって使用され、「GMPモデル」と呼ばれるgoruntineをスケジュールします。

GMPの起源

golangプログラムのgoruntineが実行のスケジューリングにGMPを必要とするのはなぜですか?

  1. シングルプロセス時代
    シングルプロセス時代(CPUはハードウェア上のシングルコア)は、プログラムがプロセスであり、他に何も存在しないことを意味します。各プロセスはオペレーティングシステムによってスケジュールされ、CPUで順番に実行され、すべてのプログラムはシリアルにのみ実行できます。
    ここに画像の説明を挿入
    このような単一の実行プロセスでは、コンピューターは一度に1つのタスクしか処理できず、プロセスのブロックはアイドル状態のCPUリソースの浪費につながります。上記の問題を解決するために、プロセスがブロックされると、オペレーティングシステムは実行を待機している他のプロセスに切り替えることができ、最も初期の同時実行機能であるマルチプロセス同時実行機能を備えています。これはこの記事の焦点では​​ありません。 、しかしそれは説明されます。

  2. マルチプロセス、マルチスレッドの時代
    マルチプロセス/マルチスレッド(CPUはハードウェア上でマルチコア)の時代では、オペレーティングシステムがブロッキングの問題を解決します。プロセスがブロックされ、CPUが切り替えて、残りの待機中のスレッドを実行できるようになり调度cpu的算法可以保证在运行的进程都可以被分配到cpu的运行时间片ます。

    ただし、マルチプロセスの同時実行には問題があります一个进程拥有太多的资源,进程的创建、切换、销毁都会占用cpu很长的时间一般に。CPUの大部分は処理进程调度使用され、実際にCPUを使用してタスクを実行する時間は十分ではありません。

    では、CPUの使用率をどのように改善できるでしょうか。時間が経つにつれて、それはマルチスレッドの時代になりました。

    プロセスは、タスクを個別に実行するために複数のスレッドを持つことができます。また、Linuxオペレーティングシステムの場合、CPUはプロセスとスレッドに対して同じ態度をとり、スケジュールすることができます。プロセスと比較して、スレッド自体が占めるリソースが少なく、CPUの切り替え速度が速いため、CPU使用率を上げるという目的を達成できます。しかし同時に、それは多くの問題をもたらす因为同一个进程中的多个线程共享资源ので、ロックや競争の衝突などの多くの問題を多线程开发设计考慮する必要があり同步竞争ます。さらに、今日の互联网高并发场景環境では、各スレッドがプロセスはもちろん、4 MBのメモリを占有する可能性があるため、タスクごとにスレッドを作成することは非現実的です。プロセス/スレッドの数が多いと、メモリ使用量が多くなり、CPU消費量が多いスケジューリングの問題が発生します。


  3. コルーチンの到着マルチスレッドの問題について説明しましたが、後でエンジニアは、スレッドが実際には「カーネルモード」スレッドと「ユーザーモード」スレッドに分割されていることを発見しました。「ユーザーモードスレッド」は「カーネルモードスレッド」にバインドする必要がありますが、CPUは「ユーザーモードスレッド」の存在を認識せず、「カーネルモードスレッド」を実行していることのみを認識します(Linux PCBプロセスコントロールピース)。詳細には、次の図に示すように、カーネルスレッドは引き続き「スレッド」と呼ばれ、ユーザースレッドは「
    ここに画像の説明を挿入
    コルーチン」と呼ばれます。コルーチンとスレッドがバインドされているため、3つのコルーチンとスレッドのマッピング関係があります。

N:1の関係:

N個のコルーチンが1つのスレッドにバインドされているという利点は、コルーチンがユーザーモードスレッドでスイッチを完了し、カーネルモードに分類されないことです。このスイッチは非常に軽量で高速です。ただし、大きなデメリットもあります。スレッドが1つしかないため、プロセスのすべてのコルーチンが1つのスレッドにバインドされます。
デメリット:
1。プログラムがハードウェアのマルチコアアクセラレーション機能を使用できない
2.コルーチンがブロックされると、スレッドブロッキングを引き起こし、このプロセスの他のコルーチンを実行できません。同時実行性は
1:1の関係ではありません。
マルチスレッド/マルチプロセスモデルでは、間違いなくコルーチンの切り替えコストが高くなります。

M:N関係:
ここに画像の説明を挿入
上記の2つのマッピング関係の欠点を克服します。
複数のコアを使用できるため、主なパフォーマンスはコルーチンスケジューラのアルゴリズムと最適化に依存します。
コルーチンはスレッドとは異なります。スレッドはCPUによってプリエンプティブにスケジュールされますが、コルーチンはユーザーモードのスケジューリングによって調整され、1つのコルーチンはあきらめます。 CPU、次のコルーチンが実行されます。しかし、golangのコルーチンは同じではありません。タイムスライスの概念があります。つまり、他のゴルンチンが飢えて死ぬのを防ぐために、ゴルーチンは最大10msのCPUを占有します。

Goruntineの紹介

Goでは、コルーチンはゴルーチンと呼ばれます。非常に軽量です。ゴルーチンは数KBしか占有せず、これらのKBはゴルーチンを実行するのに十分です。これにより、限られたメモリスペースで多数のゴルーチンをサポートし、より多くをサポートできます。並行性。ゴルーチンのスタックは数KBしか占有しませんが、実際にはスケーラブルです。より多くのコンテンツが必要な場合、ランタイムは自動的にゴルーチンを割り当てます。
Goroutineの機能:

  • より小さなメモリフットプリント(数kb)
  • より柔軟なスケジューリング(ランタイムスケジューリング)

詳細なGMP

アーリースケジューラー(GM)の紹介

次の図に示すように、元のスケジューラにはGMしかありませんでした。
ここに画像の説明を挿入

古いスケジューラのデメリット:
1。Gの作成、破棄、およびスケジューリングでは、各Mがロックを取得する必要があり、これにより激しいロック競合が発生します。

2. MをGに転送すると、遅延と追加のシステム負荷が発生します。たとえば、Gに新しいコルーチンの作成が含まれている場合、MはG 'を作成します。Gの実行を続行するには、G'をM 'に渡して実行する必要があります。これも、G'とGが関連、他のM 'ではなくMで実行するのが最善です。

3.システムコール(M間のCPU切り替え)により、スレッドのブロックおよびブロック解除操作が頻繁に発生し、システムのオーバーヘッドが増加します。

Current Scheduler(GMP)の概要

その後、スケジューラーの改良版はPを元のGMモデルに導入し、現在のGMPモデルになりました
ここに画像の説明を挿入
Goでは、スレッドはゴルーチンを実行するエンティティであり、スケジューラーの機能は、実行可能なゴルーチンをワーカースレッドに割り当てることです
。M:カーネルモードスレッド
G:コルーチンであるgoruntineと呼ばれるユーザーモードスレッドgolangの
P:スケジューラ管理、ゴルーチンを実行するためのリソースが含まれています。スレッドがゴルーチンを実行する場合は、最初にPを取得する必要があります。Pには実行可能なGキューも含まれます。
ここに画像の説明を挿入

  1. グローバルキュー:ストアGが実行を待機しています。
  2. PのローカルGキュー:グローバルキューと同様に、実行を待機しているGを格納し、ストレージの量は
    制限されてい不超过256个ます。新しいG '、G'を作成するときに优先加入到P的本地队列、キュー
    がいっぱいになると、キューがいっぱいになります把本地队列中一半的G移动到全局队列
  3. Pリスト:すべてのPは、プログラムの起動時に作成され、配列に格納され
    ます。最大でGOMAXPROCS(構成可能)があります。これはruntime.GOMAXPROCS()で設定できます。バージョン1.5より前では、シングルコア実行を使用したデフォルトは1で、デフォルトは論理CPUの最大数です。つまり、論理CPUの最大数はPです。デフォルト。
  4. Mリスト:現在のオペレーティングシステムによってgolangプログラムに割り当てられたカーネルスレッドの数。スレッドがタスクを実行する場合は、Pを取得し、PのローカルキューからGを取得する必要があります。Pキューが空の場合、Mは优先尝试グローバルキュー拿一批G放到P的本地队列またはから取得します从其他P的本地队列偷一半放到自己P的本地队列MはGを実行し、Gが実行された後、MはPから次のGを取得して繰り返します。

GoroutineスケジューラーとOSスケジューラーはMを介して結合されます。各Mはカーネルスレッドを表し、OSスケジューラーは実行のためにカーネルスレッドをCPUコアに割り当てる責任があります。

Pの数:起動時の
環境変数GOMAXPROCSまたは実行時メソッドGOMAXPROCS()によって決定されます。これは、プログラム実行中は常にGOMAXPROCSのみが存在するか、ランタイムメソッドGOMAXPROCS()によって決定されることを意味します。これは、G O M A 、X P LT R&O C SまたはであるR&LT U N- T I M E正方形方法G O M A 、X P R&LT O C S 判定セットこれはに意図された、プロセスシーケンスを実行ラインいずれかの意図したの瞬間ですのみGOMAXPROCSゴルーチンの実行で、コンセプトパラレル。論理CPUの数はデフォルトです。
Mの数:
Mの数は、プログラムの実行中に動的に変化します。Go言語自体は、デフォルトでMの最大量を10000に制限します(OSが開くことができるカーネルスレッドの数は通常10000に達しないため、基本的に制限は無視できます)。これはランタイムのSetMaxThreads関数で設定できます。 / debugパッケージ(通常は使用されません)。golangプログラムの実行中に、Mがブロックされると、新しいMが作成され、リサイクルまたはスリープされます。

MとPの数の間に絶対的な関係はありません。1つのMがブロックされると、Pは別のMを作成または切り替えます。したがって、デフォルトのPの数が1であっても、多数のMを作成できます。

PとMはいつ作成されますか

  1. Pが作成された日時:Pの最大数nを決定した後、ランタイムシステムはこの数に従ってnPを作成します。
  2. Mが作成されたのはいつですか:Pを関連付けて実行可能なGを実行するのに十分なMがありません。たとえば、この時点ですべての
    Mがブロックされており、Pには準備ができているタスクが多数ある場合、空きMが検索され、空きがない場合は新しいMが作成されます。

スケジューラの設計戦略

  • スレッドMを再利用して、スレッドの
    頻繁な作成と破棄回避しますが、スレッドを再利用します


    1. スレッドMに実行可能なGがない場合(グローバルキューが空の場合)、スレッドMを破棄するのではなく、他のスレッドにバインドされたPからGを盗もうとします(盗むメカニズム、Gレベルで動作します)。数は半分です。
      ここに画像の説明を挿入
      上の図に示すように、M2には実行可能ファイルがなく、すべてのキューが空です(グローバルキューが空でない場合は、グローバルキューの番号ルールに従います(min(len(グローバルキュー)/ GOMAXPROCS + 1、len(グローバルキュー/ 2)))取得)、それは他のMにバインドされたPローカルキューからそれ自体にバインドされたPブックのキューにゴランチンの半分を盗むためにワークスティーリングメカニズムをトリガーします

    2. ハンドオフメカニズム(MとP間のバインディング関係に作用する分離メカニズム)
      このスレッドがシステムコールを行うGによってブロックされると、スレッドはバインドされたPを解放し、実行のためにPを他のアイドルスレッドに転送します。
      下の図では、左から右にハンドオフメカニズムの実行プロセスが示されています。
      ここに画像の説明を挿入

  • 並列を
    使用マルチコアを使用して、タスクを並列に処理します。GOMAXPROCSはPの数を設定します。最大でGOMAXPROCSスレッドは複数のCPUに分散され、同時に実行されます。GOMAXPROCSは、並行性の程度も制限します。たとえば、GOMAXPROCS =コア数/ 2の場合、CPUコアの最大半分が並列処理に使用されます。

  • プリエンプション
    コルーチンでは、コルーチンがCPUを放棄するのを待ってから、次のコルーチンを実行する必要があります。Goでは、他のコルーチンが飢えて死ぬのを防ぐために、コルーチンは最大10ミリ秒のCPUを占有します。これがコルーチンとの違いです。コルーチン。

  • グローバルGキュー
    新しいスケジューラにはまだグローバルGキューがありますが、その機能は弱くなっています。Mがワークスティーリングを実行し、他のPからGを盗むことができない場合、グローバルGキューからGを取得できます。

詳細なスケジューラ

func()スケジューリングプロセスに移動します。

ここに画像の説明を挿入
上の図から、いくつかの結論を分析できます。

1. go func()を使用してゴルーチンを作成します。

2. Gを格納するための2つのキューがあります。1つはローカルスケジューラPのローカルキューで、もう1つはグローバルGキューです。新しく作成されたGは、最初にPのローカルキューに保存され、Pのローカルキューがいっぱいになると、グローバルキューに保存されます。

3. GはMでのみ実行でき、1つのMが1つのPを保持する必要があり、MとPの関係は1:1です。Mは、実行可能ファイルGをPのローカルキューからポップして実行します。Pのローカルキューが空の場合、実行可能ファイルGを他のMPの組み合わせから盗んで実行するか、グローバルキューから取得しようとします。

4. MスケジューリングG実行のプロセスは、循環メカニズムです。

5. Mが特定のGを実行するときに、システムコールまたはその他のブロック操作が発生すると、Mはブロックします。現在実行中のGがいくつかある場合、ランタイムはこのスレッドMをPから削除(デタッチ)してから、別のスレッドを作成します。このPを提供するための新しいオペレーティングシステムのスレッド(アイドルスレッドが使用可能な場合は、アイドルスレッドを再利用します)。

6. Mシステムコールが終了すると(バインドされたGはブロックされなくなります)、このMは実行するアイドルPを取得しようとし、バインドされたGをこのPのローカルキューに入れます。Pを取得できない場合、このスレッドMは休止状態になり、アイドルスレッドに参加し、このGはグローバルキューに入れられます。

スケジューラのライフサイクル:
ここに画像の説明を挿入
特別なM0およびG0
M0

M0は、プログラムの開始後に0の番号が付けられたメインスレッドです。このMに対応するインスタンスは、グローバル変数runtime.m0にあり、ヒープに割り当てる必要はありません。M0は、初期化操作の実行と最初の開始を担当します。 G.その後、M0他のMと同じ。

G0

G0は、Mが開始されるたびに作成される最初のグルチンです。G0はスケジューリングを担当するGにのみ使用され、G0は実行可能関数每个 M 都会有一个自己的 G0(MにバインドされたPのローカルキューでGをスケジュールする責任があります)を指しません。)。G0スタックスペースは、スケジューリングまたはシステムコール中に使用され、グローバル変数のG0はM0のG0です。

単純なコードによる分析:

package main

import "fmt"

func main() {
    
    
    fmt.Println("Hello world")
}
  1. ランタイムは、最初のスレッドm0とgoroutine g0を作成し、2つを関連付けます。
  2. スケジューラの初期化:m0、スタック、ガベージコレクションを初期化し、GOMAXPROCSPで構成されるPリストを作成して初期化します。
  3. サンプルコードのメイン関数はmain.mainであり、ランタイムにはメイン関数
    runtime.mainもあります。コードがコンパイルされた後、runtime.mainはmain.mainを呼び出します。プログラムが起動すると
    、ゴルーチンは次のようになります。runtime.main用に作成さ、「メインゴルーチン」と呼ばれ、メインゴルーチン
    をPのローカルキューに追加します。
  4. m0を開始し、m0はPにバインドされ、PのローカルキューからGを取得し、メインのゴルーチンを取得します。
  5. Gはスタックを所有し、MはGのスタック情報とスケジューリング情報に従って動作環境を設定します。
  6. MランG
  7. Gが終了し、再びMに戻って、実行可能なGを取得します。main.mainが終了するまでこれを繰り返します。Runtime.mainはDeferおよび
    Panic処理を実行するか、runtime.exitを呼び出してプログラムを終了します。

スケジューラーのライフサイクルは、Goプログラムのライフサイクルをほぼ満たします。runtime.mainのゴルーチンは、実行前にスケジューラーを準備するために使用されます。runtime.mainのゴルーチンは、実行時までスケジューラーの実際の開始である実行されます。 main。終わりと終わり。

参考記事:
1。https //studygolang.com/topics/12187
2. https://www.jianshu.com/p/181dc7845bb8
3. https://mp.weixin.qq.com/s/nyTF3IgPf1qkBWCJZQuTuA4
。 https://studygolang.com/topics/12057
5. https://learnku.com/articles/41728
6. https://www.bilibili.com/video/BV19r4y1w7Nx?p=4
7.GOがなぜそうなのか "速い」

ランタイムは、各関数プロローグに命令を挿入することにより、スタックスペースの継続的な操作を保証します。同時に、これらの関数のプリアンブルの命令は、スケジューラーの通常の動作も保証します。
ほとんどの場合、goroutineはスケジューラーがそれらを実行できるようにしますが、ループに関数呼び出しがない場合、スケジューラーによって切り替えられません。

おすすめ

転載: blog.csdn.net/csdniter/article/details/108248393