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

[インデックス 19620] ファイルの概要

このコミットは、Goランタイムが生成するクラッシュレポートおよびゴルーチンプロファイルにおいて、特定のOSスレッドにロックされているゴルーチン(runtime.LockOSThread()によって固定されたゴルーチン)の状態を表示するように変更を加えるものです。これにより、開発者はOSスレッドを消費しているゴルーチンを容易に特定できるようになり、デバッグやパフォーマンス分析が向上します。

コミット

Goランタイムのクラッシュレポートとゴルーチンプロファイルに、ゴルーチンがOSスレッドにロックされているかどうかの情報が追加されました。これは、runtime.LockOSThreadの誤用や、ロックされたゴルーチンのリークによってOSスレッドが消費されている状況を理解するのに役立ちます。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/07f6f313a90b264377f8a9ecc4fadfe13bfff633

元コミット内容

commit 07f6f313a90b264377f8a9ecc4fadfe13bfff633
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Jun 26 11:40:48 2014 -0700

    runtime: say when a goroutine is locked to OS thread
    Say when a goroutine is locked to OS thread in crash reports
    and goroutine profiles.
    It can be useful to understand what goroutines consume OS threads
    (syscall and locked), e.g. if you forget to call UnlockOSThread
    or leak locked goroutines.
    
    R=golang-codereviews
    CC=golang-codereviews, rsc
    https://golang.org/cl/94170043
---
 src/pkg/runtime/proc.c | 10 ++++++----\n 1 file changed, 6 insertions(+), 4 deletions(-)\n

変更の背景

Goのランタイムは、ゴルーチンをOSスレッドに多重化することで高い並行性を実現しています。しかし、特定のシナリオ(例えば、Cライブラリとの連携やGUIフレームワークの使用など、スレッドアフィニティが必要な場合)では、runtime.LockOSThread()関数を使用して、特定のゴルーチンを現在のOSスレッドに「ロック」することがあります。これにより、そのゴルーチンは他のOSスレッドに移動せず、そのOSスレッドも他のゴルーチンを実行しなくなります。

このLockOSThreadの利用は強力ですが、誤用すると問題を引き起こす可能性があります。例えば、UnlockOSThreadを呼び忘れたり、ロックされたゴルーチンがリークしたりすると、OSスレッドが不必要に消費され続け、アプリケーションのパフォーマンス低下やデッドロック、さらにはリソース枯渇につながる可能性があります。

このコミット以前は、クラッシュレポートやゴルーチンプロファイルにおいて、どのゴルーチンがOSスレッドにロックされているかを直接的に識別する情報が不足していました。そのため、開発者はLockOSThread関連の問題を診断する際に、詳細なデバッグや推測に頼る必要がありました。この変更は、そのようなデバッグ作業を簡素化し、OSスレッドを消費しているゴルーチンを明確に可視化することを目的としています。

前提知識の解説

ゴルーチン (Goroutine)

Goにおけるゴルーチンは、軽量な実行スレッドのようなものです。OSスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。Goランタイムのスケジューラが、これらのゴルーチンを少数のOSスレッドに効率的にマッピングし、実行を管理します。

OSスレッド (Operating System Thread)

OSスレッドは、オペレーティングシステムによって管理される実行単位です。CPUによって直接スケジュールされ、プロセス内で独立した実行パスを持ちます。Goのゴルーチンは、最終的にこれらのOSスレッド上で実行されます。

Goスケジューラ (Go Scheduler)

Goスケジューラは、ゴルーチンをOSスレッドに割り当て、実行を管理するGoランタイムの重要なコンポーネントです。ゴルーチンがブロックされたり、I/O操作を行ったりする際に、スケジューラは他の実行可能なゴルーチンを同じOSスレッドにスケジュールし、CPUリソースを最大限に活用します。

runtime.LockOSThread()

runtime.LockOSThread()は、現在のゴルーチンを、そのゴルーチンが現在実行されているOSスレッドに「ロック」する関数です。一度ロックされると、そのゴルーチンは他のOSスレッドに移動せず、そのOSスレッドも他のゴルーチンを実行しなくなります。この関数は、主に以下のような特定のシナリオで使用されます。

  • Cgoとの連携: Cライブラリがスレッドローカルストレージや特定のスレッドからの呼び出しを要求する場合。
  • GUIフレームワーク: OpenGLやmacOSのCocoaなど、特定のOSスレッドからの描画やイベント処理を要求するGUIライブラリを使用する場合。

LockOSThreadを使用すると、Goスケジューラの柔軟性が制限され、パフォーマンスに影響を与える可能性があるため、慎重に使用する必要があります。また、対応するruntime.UnlockOSThread()を呼び出してロックを解除しないと、OSスレッドが解放されずにリソースリークにつながる可能性があります。

クラッシュレポートとゴルーチンプロファイル

  • クラッシュレポート: Goプログラムが予期せず終了(クラッシュ)した場合に生成される情報で、スタックトレースやゴルーチンの状態などが含まれます。デバッグの重要な手がかりとなります。
  • ゴルーチンプロファイル: pprofツールなどを使用して取得できるプロファイル情報の一部で、実行中のゴルーチンの状態(実行中、待機中、システムコール中など)やスタックトレースを提供します。パフォーマンス分析やデッドロックの検出に役立ちます。

技術的詳細

このコミットの技術的な詳細は、Goランタイムがゴルーチンの状態を文字列として出力する際に、そのゴルーチンがOSスレッドにロックされているかどうかを示す情報を追加することにあります。

具体的には、ランタイム内部でゴルーチンのヘッダー情報(runtime·goroutineheader関数)を生成する際に、ゴルーチン構造体(G)のlockedmフィールドをチェックします。lockedmフィールドは、ゴルーチンがロックされているOSスレッド(M、Machineの略)へのポインタを保持しています。このポインタがnilでない場合、そのゴルーチンはOSスレッドにロックされていると判断できます。

この情報をクラッシュレポートやゴルーチンプロファイルの出力に含めることで、以下のようなメリットがあります。

  1. デバッグの効率化: LockOSThreadの誤用やリークによってOSスレッドが消費されている場合、クラッシュレポートやプロファイルを見るだけで、どのゴルーチンが問題を引き起こしているのかを一目で特定できるようになります。これにより、問題の切り分けと解決にかかる時間を大幅に短縮できます。
  2. リソース消費の可視化: アプリケーションが予期せず多数のOSスレッドを消費している場合、この情報によって、それがLockOSThreadによるものなのか、あるいは他の原因によるものなのかを区別できるようになります。
  3. パフォーマンス分析の深化: ゴルーチンプロファイルにおいて、ロックされたゴルーチンの存在が明らかになることで、スケジューラの動作やスレッドの利用状況をより深く分析し、最適化の機会を見つけることができます。

この変更は、Goランタイムの診断能力を向上させ、開発者がより堅牢で効率的なGoアプリケーションを構築するのを支援します。

コアとなるコードの変更箇所

変更はsrc/pkg/runtime/proc.cファイルに集中しています。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -297,10 +297,12 @@ runtime·goroutineheader(G *gp)\n \tif((gp->status == Gwaiting || gp->status == Gsyscall) && gp->waitsince != 0)\n \t\twaitfor = (runtime·nanotime() - gp->waitsince) / (60LL*1000*1000*1000);\n \n-\tif(waitfor < 1)\n-\t\truntime·printf(\"goroutine %D [%s]:\\n\", gp->goid, status);\n-\telse\n-\t\truntime·printf(\"goroutine %D [%s, %D minutes]:\\n\", gp->goid, status, waitfor);\n+\truntime·printf(\"goroutine %D [%s\", gp->goid, status);\n+\tif(waitfor >= 1)\n+\t\truntime·printf(\", %D minutes\", waitfor);\n+\tif(gp->lockedm != nil)\n+\t\truntime·printf(\", locked to thread\");\n+\truntime·printf(\"]:\\n\");\n }\n \n void

この差分からわかるように、runtime·goroutineheader関数内のruntime·printfの呼び出しが変更されています。

コアとなるコードの解説

src/pkg/runtime/proc.cは、Goランタイムのプロセス管理に関連するコードが含まれるファイルです。これには、ゴルーチンのスケジューリング、OSスレッドとの連携、およびデバッグ情報の出力に関する低レベルの機能が含まれます。

変更が加えられたruntime·goroutineheader関数は、クラッシュレポートやプロファイル出力において、個々のゴルーチンのヘッダー情報をフォーマットして出力する役割を担っています。この関数は、ゴルーチンのID (gp->goid)、現在のステータス (status)、および待機時間 (waitfor) などの情報を表示します。

変更前は、ゴルーチンのID、ステータス、そして必要に応じて待機時間のみが出力されていました。

// 変更前
if(waitfor < 1)
    runtime·printf("goroutine %D [%s]:\n", gp->goid, status);
else
    runtime·printf("goroutine %D [%s, %D minutes]:\n", gp->goid, status, waitfor);

変更後では、runtime·printfの呼び出しが複数に分割され、より詳細な情報を条件付きで追加できるようになりました。

// 変更後
runtime·printf("goroutine %D [%s", gp->goid, status); // 基本情報
if(waitfor >= 1)
    runtime·printf(", %D minutes", waitfor); // 待機時間(1分以上の場合)
if(gp->lockedm != nil)
    runtime·printf(", locked to thread"); // OSスレッドにロックされている場合
runtime·printf("]:\n"); // 閉じ括弧と改行

この変更の核心は、if(gp->lockedm != nil)という条件分岐です。

  • gpは現在のゴルーチン(G構造体)へのポインタです。
  • gp->lockedmは、このゴルーチンがロックされているOSスレッド(M構造体)へのポインタです。
  • nilはポインタが何も指していない状態、つまりゴルーチンがOSスレッドにロックされていないことを意味します。

したがって、gp->lockedm != nilが真の場合、そのゴルーチンは特定のOSスレッドにロックされていることを示します。この条件が満たされたときに、, locked to threadという文字列がゴルーチンのヘッダー情報に追加されます。

このシンプルな変更により、Goランタイムの診断出力が大幅に強化され、runtime.LockOSThread()の利用状況に関する重要な情報が提供されるようになりました。

関連リンク

参考にした情報源リンク