[インデックス 16376] ファイルの概要
このコミットは、GoランタイムにおけるCgo(C言語との相互運用機能)使用時のデッドロック検出の挙動を改善するものです。具体的には、Cgoが使用された際にGoランタイムが追加のM(Machine、OSスレッドに相当)を生成するタイミングを、プログラムの起動時ではなく、最初のCgo呼び出し時(遅延ロード)に変更することで、デッドロック検出器が不必要に無効化されるのを防ぎます。これにより、Cgoをインポートしているだけで実際には使用していないプログラムや、Goのレース検出器を使用しているプログラムにおいても、デッドロック検出が正しく機能するようになります。
コミット
commit 34c67eb24e1b3cfe16a512ab2d4899c78032030b
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed May 22 22:57:47 2013 +0400
runtime: detect deadlocks in programs using cgo
When cgo is used, runtime creates an additional M to handle callbacks on threads not created by Go.
This effectively disabled deadlock detection, which is a right thing, because Go program can be blocked
and only serve callbacks on external threads.
This also disables deadlock detection under race detector, because it happens to use cgo.
With this change the additional M is created lazily on first cgo call. So deadlock detector
works for programs that import "C", "net" or "net/http/pprof" but do not use them in fact.
Also fixes deadlock detector under race detector.
It should be fine to create the M later, because C code can not call into Go before first cgo call,
because C code does not know when Go initialization has completed. So a Go program need to call into C
first either to create an external thread, or notify a thread created in global ctor that Go
initialization has completed.
Fixes #4973.
Fixes #5475.
R=golang-dev, minux.ma, iant
CC=golang-dev
https://golang.org/cl/9303046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/34c67eb24e1b3cfe16a512ab2d4899c78032030b
元コミット内容
Goランタイムにおいて、Cgoを使用するプログラムでのデッドロック検出を改善します。
Cgoが使用されると、Goによって作成されていないスレッドからのコールバックを処理するために、ランタイムは追加のM(OSスレッド)を作成していました。これは、Goプログラムがブロックされ、外部スレッドからのコールバックのみを処理できる場合があるため、デッドロック検出を実質的に無効にしていました。また、レース検出器もCgoを使用しているため、レース検出器下でもデッドロック検出が無効になっていました。
この変更により、追加のMは最初のCgo呼び出し時に遅延して作成されるようになります。これにより、"C"
、"net"
、"net/http/pprof"
などをインポートしているものの、実際には使用していないプログラムでもデッドロック検出が機能するようになります。また、レース検出器下でのデッドロック検出も修正されます。
CコードはGoの初期化が完了したことを知らないため、最初のCgo呼び出しの前にGoを呼び出すことはできません。したがって、Goプログラムが最初にCを呼び出すか、グローバルコンストラクタで作成されたスレッドにGoの初期化が完了したことを通知する必要があるため、Mの作成を遅らせても問題ありません。
Issue #4973 と #5475 を修正します。
変更の背景
Goランタイムは、プログラムがデッドロック状態にあるかどうかを検出する機能を持っています。しかし、Cgo(GoとC言語の相互運用機能)を使用するプログラムでは、このデッドロック検出が意図せず無効になるという問題がありました。
従来のGoランタイムでは、Cgoが使用されることが検出されると、Goによって管理されていない外部スレッドからのコールバックを処理するために、起動時に追加のM(OSスレッド)を生成していました。この追加のMの存在が、デッドロック検出のロジックに影響を与えていました。Goのデッドロック検出は、すべてのGoルーチンがブロックされ、かつ実行可能なMが存在しない場合にデッドロックと判断します。しかし、Cgoによって生成された追加のMが存在すると、たとえGoルーチンがすべてブロックされていても、このMが外部からのコールバックを待機している可能性があるため、ランタイムはデッドロックと判断せず、デッドロック検出が機能しなくなっていました。
この問題は、特に以下のようなシナリオで顕著でした。
- Cgoをインポートしているが実際には使用していないプログラム: 例えば、
"net"
パッケージや"net/http/pprof"
パッケージは内部的にCgoを使用している場合があります。これらのパッケージをインポートするだけで、Cgoの機能が実際に呼び出されていなくても、追加のMが生成され、デッドロック検出が無効になっていました。 - Goのレース検出器を使用しているプログラム: Goのレース検出器も内部的にCgoを使用しているため、レース検出器を有効にすると、デッドロック検出が機能しなくなるという副作用がありました。
これらの問題を解決し、Cgoを使用するプログラムでもデッドロック検出が正しく機能するようにするために、追加のMの生成タイミングを遅延させる変更が導入されました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とCgoに関する知識が必要です。
Goランタイムのスケジューラ (GMPモデル)
Goランタイムは、Goルーチン(G)、論理プロセッサ(P)、OSスレッド(M)の3つの要素からなるスケジューラモデル(GMPモデル)を採用しています。
- G (Goroutine): GoルーチンはGoプログラムの実行単位です。軽量なスレッドのようなもので、数百万個作成することも可能です。
- P (Processor): 論理プロセッサは、Goルーチンを実行するためのコンテキストを提供します。PはGoルーチンをMにディスパッチし、MがGoルーチンを実行します。
GOMAXPROCS
環境変数によってPの数を設定できます。 - M (Machine): OSスレッドに相当します。MはPにアタッチされ、PがディスパッチしたGoルーチンを実行します。MはシステムコールやCgo呼び出しなどでブロックされることがあります。
Goランタイムのスケジューラは、GをPに、PをMに割り当てることで、効率的な並行処理を実現しています。デッドロック検出は、すべてのGがブロックされ、かつ実行可能なMが存在しない場合に発動します。
Cgo (GoとC言語の相互運用)
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのメカニズムです。Cgoを使用すると、Goの標準ライブラリでは提供されていないOSの機能や、既存のCライブラリを利用することができます。
Cgoの呼び出しは、Goランタイムのスケジューラに特別な考慮を必要とします。GoルーチンがC関数を呼び出すと、そのGoルーチンを実行しているMはCコードの実行中にブロックされる可能性があります。このとき、Goランタイムは、ブロックされたMからPをデタッチし、新しいMを生成して別のPにアタッチすることで、他のGoルーチンの実行を継続できるようにします。
また、C言語側からGoの関数をコールバックするシナリオも存在します。この場合、Goランタイムによって管理されていないOSスレッド(C言語側で作成されたスレッドなど)からGoの関数が呼び出されることになります。Goランタイムは、このような外部スレッドからのコールバックを処理するために、追加のMを必要とします。
デッドロック検出
Goランタイムには、プログラムがデッドロック状態に陥ったことを検出する機能があります。デッドロックとは、複数のGoルーチンが互いに相手が保持しているリソースの解放を待機し、結果としてどのGoルーチンも処理を進められなくなる状態を指します。
Goのデッドロック検出器は、すべてのGoルーチンがブロックされており、かつ実行可能なMが一つも存在しない場合に、デッドロックと判断してプログラムを終了させます。これは、Goルーチンがブロックされているにもかかわらず、Mがシステムコールなどでブロックされていない場合、Goルーチンはいつか実行される可能性があるため、デッドロックとは判断しないというロジックに基づいています。
レース検出器 (Race Detector)
Goのレース検出器は、並行処理におけるデータ競合(レースコンディション)を検出するためのツールです。データ競合は、複数のGoルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつアクセスが同期されていない場合に発生します。レース検出器は、プログラムの実行中にこれらの競合を検出し、レポートします。
レース検出器は、その実装の都合上、内部的にCgoを使用しています。そのため、レース検出器を有効にすると、Cgoが使用されているとGoランタイムが判断し、前述のデッドロック検出の問題が発生していました。
技術的詳細
このコミットの核心は、Cgo使用時に追加のMを生成するタイミングを、プログラム起動時ではなく、最初のCgo呼び出し時(オンデマンド)に変更することです。
変更前は、GoランタイムがCgoを使用するプログラムであることを検出すると、runtime·mstart
関数内でruntime·newextram()
を呼び出し、追加のMを起動時に生成していました。このMは、Goによって作成されていないスレッドからのコールバックを処理するために存在します。しかし、このMが常に存在することで、たとえGoルーチンがすべてブロックされていても、このMが外部からのコールバックを待機していると判断され、デッドロック検出器が機能しなくなっていました。
変更後は、runtime·needextram
という新しいグローバル変数(uint32
型)が導入され、初期値は1
に設定されます。この変数は、追加のMが必要かどうかを示すフラグとして機能します。
runtime·iscgo.c
:runtime·needextram
変数が宣言され、初期値1
が設定されます。uint32 runtime·needextram = 1; // create an extra M on first cgo call
runtime·cgocall.c
:runtime·cgocall
関数(GoからCを呼び出す際の内部関数)の冒頭で、runtime·needextram
の値をチェックします。runtime·needextram
が1
であり、かつruntime·cas(&runtime·needextram, 1, 0)
(Compare-And-Swap操作でruntime·needextram
を1
から0
にアトミックに設定)が成功した場合にのみ、runtime·newextram()
が呼び出され、追加のMが生成されます。- このCAS操作により、複数のGoルーチンが同時にCgo呼び出しを行ったとしても、
runtime·newextram()
は一度だけ実行されることが保証されます。
runtime·proc.c
:runtime·mstart
関数から、起動時のruntime·newextram()
の呼び出しが削除されます。これにより、Cgoがインポートされていても、実際にCgo呼び出しが行われるまでは追加のMは生成されません。runtime·needm
関数(Goルーチンを実行するためのMが必要になったときに呼び出される)に新しいチェックが追加されます。もしruntime·needextram
がまだ1
(つまり、まだ最初のCgo呼び出しが行われていない)の状態で、C/C++コードがGoのグローバルコンストラクタからGoを呼び出そうとした場合、これは予期せぬ動作であるため、致命的なエラーとしてプログラムを終了させます。これは、CコードがGoの初期化完了を待たずにGoを呼び出すべきではないという前提に基づいています。
この変更により、Cgoをインポートしているだけで実際には使用していないプログラムでは、追加のMが生成されないため、デッドロック検出器が常に有効になります。また、レース検出器を使用している場合でも、最初のCgo呼び出し(レース検出器の内部的なCgo使用)が行われたときにのみ追加のMが生成されるため、それまでの間はデッドロック検出が機能し、その後も必要に応じてMが管理されることで、デッドロック検出の精度が向上します。
コアとなるコードの変更箇所
このコミットでは、以下の4つのファイルが変更されています。
src/pkg/runtime/cgo/iscgo.c
:uint32 runtime·needextram = 1;
という新しいグローバル変数が追加されました。これは、追加のM(OSスレッド)がまだ作成されていないことを示すフラグです。
src/pkg/runtime/cgocall.c
:runtime·cgocall
関数の内部に、runtime·needextram
をチェックし、必要であればruntime·newextram()
を呼び出すロジックが追加されました。これは、最初のCgo呼び出し時に追加のMを遅延作成するためのものです。
src/pkg/runtime/proc.c
:
runtime·mstart
関数から、Cgoが有効な場合に起動時にruntime·newextram()
を呼び出す行が削除されました。runtime·needextram
変数の外部宣言が追加されました。runtime·needm
関数に、runtime·needextram
がまだ1
の状態でCgoコールバックが発生した場合の致命的なエラーチェックが追加されました。
src/pkg/runtime/runtime.h
:runtime·needextram
変数の外部宣言が追加されました。
コアとなるコードの解説
src/pkg/runtime/cgo/iscgo.c
#include "../runtime.h"
bool runtime·iscgo = 1;
+uint32 runtime·needextram = 1; // create an extra M on first cgo call
runtime·iscgo
はGoプログラムがCgoを使用しているかどうかを示すフラグです。このコミットでは、runtime·needextram
という新しいグローバル変数が追加されています。この変数はuint32
型で、初期値は1
です。この1
という値は、「追加のMがまだ作成されておらず、最初のCgo呼び出し時に作成する必要がある」ことを示します。
src/pkg/runtime/cgocall.c
// Create an extra M for callbacks on threads not created by Go on first cgo call.
if(runtime·needextram && runtime·cas(&runtime·needextram, 1, 0))
runtime·newextram();
これはruntime·cgocall
関数(GoからC関数を呼び出す際にGoランタイムが内部的に使用する関数)に追加されたコードです。
runtime·needextram
: この変数が1
であるか(つまり、まだ追加のMが作成されていないか)をチェックします。runtime·cas(&runtime·needextram, 1, 0)
: これはCompare-And-Swap(CAS)操作です。runtime·needextram
変数の現在値が1
であれば、それをアトミックに0
に設定します。この操作が成功した場合(つまり、他のGoルーチンが先にMを作成していなかった場合)にのみ、runtime·newextram()
が呼び出されます。 このロジックにより、複数のGoルーチンが同時にCgo呼び出しを行ったとしても、runtime·newextram()
は一度だけ実行されることが保証され、追加のMが遅延かつ安全に作成されます。
src/pkg/runtime/proc.c
@@ -475,11 +476,8 @@ runtime·mstart(void)
// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
-\tif(m == &runtime·m0) {\n+\tif(m == &runtime·m0)\n \t\truntime·initsig();
-\t\tif(runtime·iscgo)\n-\t\t\truntime·newextram();
-\t}\n
+\t
\tif(m->mstartfn)\n \t\tm->mstartfn();
runtime·mstart
関数は、新しいM(OSスレッド)が起動したときに実行される初期化関数です。変更前は、runtime·m0
(最初のM)が起動し、かつCgoが有効な場合に、ここでruntime·newextram()
が呼び出されていました。このコミットでは、この起動時のruntime·newextram()
の呼び出しが削除されました。これにより、追加のMの生成がCgo呼び出し時まで遅延されるようになります。
@@ -587,6 +585,14 @@ runtime·needm(byte x)
{
M *mp;
+\tif(runtime·needextram) {
+\t\t// Can happen if C/C++ code calls Go from a global ctor.
+\t\t// Can not throw, because scheduler is not initialized yet.
+\t\truntime·write(2, "fatal error: cgo callback before cgo call\n",
+\t\t\tsizeof("fatal error: cgo callback before cgo call\n")-1);
+\t\truntime·exit(1);
+\t}
+\n
// Lock extra list, take head, unlock popped list.
// nilokay=false is safe here because of the invariant above,
// that the extra list always contains or will soon contain
runtime·needm
関数は、Goルーチンを実行するためのMが必要になったときに呼び出されます。ここに追加されたコードは、C/C++コードがGoのグローバルコンストラクタなどから、最初のCgo呼び出しが行われる前にGoの関数をコールバックしようとした場合のチェックです。
if(runtime·needextram)
:runtime·needextram
がまだ1
である(つまり、追加のMがまだ作成されていない)ことを確認します。- この条件が真の場合、それはCコードがGoの初期化が完了する前にGoを呼び出そうとしていることを意味します。これはGoランタイムの想定外の動作であり、スケジューラがまだ完全に初期化されていない可能性があるため、回復不能なエラーとして扱われます。
runtime·write
とruntime·exit
を使って、エラーメッセージを標準エラー出力に書き込み、プログラムを終了させます。
src/pkg/runtime/runtime.h
extern G* runtime·lastg;
extern M* runtime·allm;
extern P** runtime·allp;
extern int32 runtime·gomaxprocs;
+extern uint32 runtime·needextram;
extern bool runtime·singleproc;
extern uint32 runtime·panicking;
extern uint32 runtime·gcwaiting; // gc is waiting to run
runtime·needextram
変数の外部宣言が追加されました。これにより、他のランタイムファイルからこの変数にアクセスできるようになります。
関連リンク
- https://github.com/golang/go/commit/34c67eb24e1b3cfe16a512ab2d4899c78032030b
- https://golang.org/cl/9303046
- Issue #4973: https://code.google.com/p/go/issues/detail?id=4973 (現在はGitHubに移行しているため、GitHubのIssueページを参照)
- Issue #5475: https://code.google.com/p/go/issues/detail?id=5475 (現在はGitHubに移行しているため、GitHubのIssueページを参照)
参考にした情報源リンク
- Go runtime scheduler (M, P, G model):
- Cgo:
- Go deadlock detection:
- Go race detector:
- Compare-And-Swap (CAS) operation: