Work"という発表をしてきました。Processesについての話しをする予定だったのですが、時間内にとても発表できそうな内容ではなかったため、Concurrency全般についての話をしました。そのため、ここではその際触れられなかったgoroutineの実装の話しやCSPの話しなどを含めてGoのruntimeについて何回かに分けてまとめていきたいと思います。今回は主にgoroutineについてです。GoのConcurrencygoroutineの説明に入る前にざっくりGoのConcurrencyについて説明します。以下、GoConでの発表スライドにざっくりと沿いながら書いていきます。communicating."というものがあるのはみなさんご存じでしょう。これはgo-proverbsというGoらしいコードを書くためのことわざ集にも書いてあり、とても有名な格言かと思います。Communicationしようねと言っています。Communicationそれぞれの特徴とGoにおいてそれぞれの手法がどのように実現されているのかを理解しなければいけません。Communicationの違いなどは発表資料を参考にしてください。Communicationをどのように実現しているんでしょうか。ポイントとなるのは以下の機能です。Communicationをより簡単に使えるようになっています。今回の記事ではこの中のgoroutineに焦点を当てます。(続編でchannelについても扱う予定です)なお、発表でも触れましたがselectの実践的な使い方に関しては牧さんの以下の発表が詳しいです。Communicationを実現するための方法としてsync.Mutexなどが提供されています。今回はMessaging-passingに焦点を当てるためここではあまり深くは扱いません。詳しくはドキュメントを読んでください、と言いたいところではありますが英語に抵抗がある方などは、少し前の記事にはなりますがmattnさんによるsyncパッケージについての記事などもあるのでそちらを読むといいと思います。mattn.kaoriya.netgoroutineの実装さて、やっと本題のgoroutineについてです。goroutineを知るためにはsrc/runtime以下を読まなければいけません。runtimeのドキュメントは以下です。あるスレッドのデフォルトスタックサイズにくらべてとても軽量です。OSスレッドはOSカーネルでスケジュールされており、スレッド間の制御を変更するには完全なコンテキストスイッチが必要なので遅くなってしまうのですが、goroutineは以下で説明するM:Nスレッド(LWP方式)を用いているため、スレッドの再スケジュールより低コストにスケジューリング可能という特徴があります。スレッドの種類と特徴スレッドには大きく分けてN:1と1:1、そしてM:N方式があります。トラップ(強制的割り込み)する必要があるため、コンテキストスイッチが遅いM:N任意の数のOSスレッドに任意の数のゴルーチンをスケジューリングします。runtime以下の説明をしていくにあたって主要な登場人物が3人(!?)います。この子がG(Goroutine)です。gは空いているgのpoolに戻されて、後で他のゴルーチンのために再利用されます。スタック内には、命令ポインタおよびゴルーチンのスケジューリングに重要な情報が含まれます。(例:ブロックされている可能性があるチャネルの情報等)ちなみにこれらはsrc/runtime/runtime2.goというファイルに記述されています。runtime以下には他にruntime.goとruntime1.goというファイルが存在します。mOS}M(Machine)です。OSスレッドを表します。ユーザーのGoコード、runtimeのコード、syscallを実行しているか、idle状態になっています。複数のスレッドがシステムコールでブロックされる可能性があるため、一度に複数のMが存在する可能性があります。[sys.CacheLineSize]byte}P(processor)です。スケジューラおよびメモリアロケータの状態など、ユーザGoコードを実行するために必要なリソースを表し、pと書かれます。単一のスレッドでGoコードを実行するスケジューラのローカライズ版のようなものだとイメージしてください。pの内容はCPUごとの状態のように考えることができ、スレッド単位またはゴルーチン単位である必要はない状態を管理するのに適しています。N:1スケジューラからM:Nスケジューラに移行することができる重要な部分です。Pによって、Goプロセスの呼び出しを個々のコンピュータに合わせることができます。Pの数は起動時にGOMAXPROCS環境変数の値または実行時関数GOMAXPROCS()によって設定されます。あとでも説明しますがGOMAXPROCSはあくまでもPの値であり、Mではありません。GOMAXPROCSが1でも複数のOSスレッドで実行されることはありますスケジューラーGとMとPがただ存在するだけでは我々の使っている並行処理機構は成立しえません。実行するコードであるG、実行する場所であるM、それを実行する権利とリソースであるPをうまく組み合わせてあげる必要があります。そこでスケジューラーの出番です。ユーザースタックとシステムスタックactiveなGにはGoコードが実行する最小で2048byteのユーザースタックが関連付けられていて、動的に増減します。"gsignal"スタックとして実装されています。システムスタックとシグナルスタックは大きく拡大することはできませんが、ランタイムとcgoコードを実行するのに十分な大きさがあります。asmcgocallなどを使用して、一時的にシステムスタックに切り替えてユーザーのゴルーチンを切り替えるタスクを実行します。システムスタック上で処理が実行されている間はユーザースタックは実行に使用されません。goroutineが切り替わるタイミング上記のタイミングでgoroutineの切り替え作業が行われるのですが、ざっくりとまとめると以下のようなタイミングで切り替えが行われます。ディスクI/Oとか待ちが入る余地がない即座に帰ってくる系のものだとスイッチしませんメモリの割り当てが行われるtime.Sleep()が呼ばれるruntime.Gosched()が呼ばれるGAEのようなGOMAXPROCS(=P)が1の時にこの切り替えが呼ばれない処理を並行処理しようとしても逐次処理と同じ動作をしてしまうので注意が必要です。同期方法runtimeには複数の同期メカニズムがあります。セマンティクス、特にゴルーチンスケジューラまたはOSスケジューラと相互作用するかどうかが異なります。mutexロックとアンロックを使う単純な方法で、共有部分を短期間保護するために使用されます。mutexを用いるとGoスケジューラとやりとりすることなくMが直接ブロックされるのでruntimeの最下位レベルから使用する分には安全ですが、関連付けられたGおよびPの再スケジューリングもブロックされてしまいます。notificationsという手法があります。notesleepは、関連付けられたGとPの再スケジュールをブロックしてしまいますが、notetsleepgはブロックシステムコールのように動作し、別のGを実行するためにPを再利用できるようにします。こちらの手法はMを消費するのでGを直接ブロックする方法よりは効率的ではありません。N動作例イメージが湧きやすいように例を示します。blogから引用したものです。G,M,Pはそれぞれ以下の様なアイコンで表します。通常状態(GOMAXPROCS=2)青色のGが実行中のGで、PはGo文が実行されるたびにrunqueuesというキューのリストからGをポップします。GOMAXPROCS=2なのでPは2つ存在します。Pがスケジューリングポイント(メモリが一貫性をもつポイント)までゴルーチンを実行すると、その実行キューからGがポップされ、スタックと命令ポインタが設定され、ゴルーチンの実行が開始されます。このPの持つGのリストをローカルの実行キューと呼び、これ以外にグローバルの実行キューが存在します。Pはローカル実行キューを使い果たしたときにグローバルキューからGを引き出します。旧バージョンのGoスケジューラではグローバル実行キューしかありませんでしたが、1.1からローカル実行キューが追加されました。Pによるsteal以下の図の左側では、片方のPからGが無くなって手持ちぶさたになってしまっています。待機しているGが無くなるとPは別のPから実行キューの約半分を奪います。これによって、コンテキストごとに常に作業が行われるようになり、すべてのスレッドが最大容量で動作することができます。syscall以下はG0でsyscallが呼ばれた場合です。Mはコードを実行していてシステムコールでブロックすることができないため、スケジューリングを維持できるようにPをハンドオフする必要があります。M0はシステムコールを作成したgoroutineを保持したままPをリストにプッシュして、それをM1がポップして使用します。この左図から、GOMAXPROCS(=P)が1であってもGoプログラムは複数のスレッド(M)で実行されることがわかります。最後にいかがだったでしょうか。今回、メモリ割り当てやGC、コンパイラ命令の話しはあまり需要がなさそうなので触れませんでしたが、runtimeがGoの奥深さが詰まった世界であることが少しでも伝わったら幸いです。runtimeを読んでGoにコミットをしてみるのも楽しいかなと思います。crashで実行すると便利です。現在続編としてchannelとCSPに関する記事を書いています。commitsの方も二週間空いてしまいそうなので、気が向いたら更新したいです。Twitterでもお問い合わせフォームにでもいいので連絡ください。#golang3/20~3/28のコミットから取り上げています。niconegoto注目記事はてなブログをはじめよう!niconegotoさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。