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

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

このコミットは、Goランタイムにおけるネットワークポーラーのデッドロック問題を修正するものです。Goスケジューラが持つ重要な不変条件(invariant)の一つである「常に少なくとも1つの実行中のP(論理プロセッサ)またはネットワークをポーリングしているスレッドが存在する」という条件が破られた場合に発生するデッドロックを解消します。具体的には、handoffp関数内で、最後の実行中のPがアイドル状態になろうとしている際に、ネットワークポーリングを行うM(OSスレッド)が存在しない場合に、新しいMを起動してネットワークポーリングを継続させるロジックを追加しています。

コミット

commit 0b5d55984fc939fdc35128342aa7cb34b0798de6
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sat Apr 6 22:27:54 2013 -0700

    runtime: fix deadlock in network poller
    The invariant is that there must be at least one running P or a thread polling network.
    It was broken.
    Fixes #5216.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/8459043

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

https://github.com/golang/go/commit/0b5d55984fc939fdc35128342aa7cb34b0798de6

元コミット内容

runtime: fix deadlock in network poller
The invariant is that there must be at least one running P or a thread polling network.
It was broken.
Fixes #5216.

R=golang-dev, bradfitz, r
CC=golang-dev
https://golang.org/cl/8459043

変更の背景

Goランタイムは、ゴルーチン(Goの軽量スレッド)のスケジューリングを効率的に行うために、M(OSスレッド)、P(論理プロセッサ)、G(ゴルーチン)という3つの主要な抽象化を使用します。MはOSスレッドを表し、Pはゴルーチンを実行するためのコンテキスト(スケジューラキュー、メモリキャッシュなど)を提供します。Gは実際に実行されるゴルーチンです。

Goランタイムには、システムが完全にアイドル状態に陥り、外部からのイベント(特にネットワークI/O)を処理できなくなることを防ぐための重要な不変条件が存在します。それは、「常に少なくとも1つのPが実行中であるか、またはネットワークI/OをポーリングしているOSスレッド(M)が存在しなければならない」というものです。この不変条件が破られると、システムは外部からのイベントに応答できなくなり、デッドロック状態に陥る可能性があります。

このコミットは、この不変条件が特定のシナリオで破られ、ネットワークポーラーがデッドロックを引き起こす可能性があった問題を修正するために導入されました。具体的には、すべてのPがアイドル状態になり、かつネットワークポーリングを担当するMも存在しない場合に、システムが停止してしまう状況が発生していました。

コミットメッセージに記載されている Fixes #5216 は、当時のGoの課題追跡システムにおける特定のバグ報告を指しています。このコミットは、その報告されたデッドロック問題を解決するために作成されました。

前提知識の解説

Goランタイムスケジューラ (M, P, G)

  • G (Goroutine): Goにおける並行処理の単位。非常に軽量で、数百万個作成することも可能です。
  • P (Processor): 論理プロセッサ。ゴルーチンを実行するためのコンテキストを提供します。Pは、実行可能なゴルーチンのローカルキューを持ち、Mがゴルーチンを実行する際にPにアタッチされます。GOMAXPROCS環境変数によって設定されるPの数は、同時に実行できるゴルーチンの数を制限します。
  • M (Machine/OS Thread): OSスレッド。Pにアタッチされ、Pが提供するコンテキスト上でゴルーチンを実行します。Mは、システムコールやブロッキングI/Oが発生した場合にPからデタッチされ、別のMがそのPにアタッチされてゴルーチンの実行を継続できます。

ネットワークポーラー

Goランタイムには、ノンブロッキングI/Oを効率的に処理するためのネットワークポーラー(NetPoller)が組み込まれています。これは、epoll (Linux), kqueue (FreeBSD/macOS), IOCP (Windows) などのOSのI/O多重化メカニズムを利用して、複数のネットワーク接続からのイベントを監視します。ネットワークI/Oが準備できるまでゴルーチンをブロックせず、他のゴルーチンを実行できるようにすることで、高い並行性を実現します。

デッドロック

デッドロックとは、複数のプロセスやスレッドが互いに相手が保持しているリソースの解放を待っている状態になり、結果としてどのプロセスも処理を進められなくなる状態を指します。Goランタイムの文脈では、すべてのMがブロッキング状態に陥り、かつ新しいMを起動するトリガーも存在しない場合にデッドロックが発生する可能性があります。

不変条件 (Invariant)

不変条件とは、プログラムの実行中、常に真であると期待される条件のことです。Goランタイムスケジューラにおける「常に少なくとも1つの実行中のPまたはネットワークをポーリングしているスレッドが存在する」という不変条件は、システムが外部イベントに応答し続け、デッドロックを回避するために不可欠です。

技術的詳細

このコミットが修正する問題は、GoランタイムのスケジューラがP(論理プロセッサ)をアイドル状態にする際のロジック、特にhandoffp関数に関連しています。handoffp関数は、現在のPがアイドル状態になる際に、そのPを他のMに引き渡すか、またはアイドルPのリストに戻すかを決定します。

問題の核心は、すべてのPがアイドル状態になり、かつネットワークポーリングを担当するMも存在しない場合に、システムが完全に停止してしまう可能性があった点です。

Goランタイムは、ネットワークI/Oを処理するために、必要に応じて特別なM(ネットワークポーラーM)を起動します。このMは、ネットワークイベントを監視し、準備ができたゴルーチンをスケジューラに投入します。しかし、もしすべてのPがアイドル状態になり、かつネットワークポーラーMも存在しない場合、新しいネットワークイベントが発生しても、それを処理するMが起動されず、結果としてデッドロックが発生します。

コミットが追加したコードは、この特定のシナリオを検出して対処します。

  • runtime·sched.npidle: 現在アイドル状態にあるPの数。
  • runtime·gomaxprocs: システムが利用できるPの総数。
  • runtime·atomicload64(&runtime·sched.lastpoll): ネットワークポーラーが最後にポーリングを行った時刻(または関連するフラグ)。これが0でない場合、ネットワークポーラーがアクティブであるか、少なくとも過去にアクティブであったことを示唆します。

追加された条件 runtime·sched.npidle == runtime·gomaxprocs-1 は、「現在アイドル状態にあるPの数が、利用可能なPの総数から1を引いた数に等しい」ことを意味します。これは、handoffpが呼び出されている現在のPがアイドル状態になろうとしているため、このPがアイドル状態になると、すべてのPがアイドル状態になることを示します。

そして、runtime·atomicload64(&runtime·sched.lastpoll) != 0 は、ネットワークポーラーが最後にポーリングを行ってから時間が経過している(つまり、現在アクティブなネットワークポーラーMが存在しない可能性が高い)ことを示します。

この両方の条件が真である場合、つまり「現在のPがアイドル状態になるとすべてのPがアイドル状態になり、かつネットワークポーラーもアクティブでない」という状況であれば、ランタイムはデッドロックを回避するために、新しいMを起動してネットワークポーリングを行わせる必要があります。

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

src/pkg/runtime/proc.c ファイルの handoffp 関数に以下の7行が追加されました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -875,6 +875,13 @@ handoffp(P *p)
 		startm(p, false);
 		return;
 	}
+	// If this is the last running P and nobody is polling network,
+	// need to wakeup another M to poll network.
+	if(runtime·sched.npidle == runtime·gomaxprocs-1 && runtime·atomicload64(&runtime·sched.lastpoll) != 0) {
+		runtime·unlock(&runtime·sched);
+		startm(p, false);
+		return;
+	}
 	pidleput(p);
 	runtime·unlock(&runtime·sched);
 }

コアとなるコードの解説

追加されたコードブロックは、handoffp 関数内でPがアイドル状態になる直前に実行されます。

	// If this is the last running P and nobody is polling network,
	// need to wakeup another M to poll network.
	if(runtime·sched.npidle == runtime·gomaxprocs-1 && runtime·atomicload64(&runtime·sched.lastpoll) != 0) {
		runtime·unlock(&runtime·sched);
		startm(p, false);
		return;
	}
  1. if(runtime·sched.npidle == runtime·gomaxprocs-1 && runtime·atomicload64(&runtime·sched.lastpoll) != 0):

    • runtime·sched.npidle == runtime·gomaxprocs-1: これは、現在処理中のPがアイドル状態になると、システム内のすべてのPがアイドル状態になることを意味します。つまり、他にゴルーチンを実行できるPが存在しなくなる状況です。
    • runtime·atomicload64(&runtime·sched.lastpoll) != 0: これは、ネットワークポーラーが最後にポーリングを行ってから時間が経過しており、現在アクティブなネットワークポーラーMが存在しない可能性が高いことを示します。lastpollが0でないということは、ポーラーが一度は起動したが、現在は活動していない状態を示唆します。
    • このif文は、システムが完全にアイドル状態に陥り、かつネットワークI/Oを処理するMも存在しないという、デッドロックの危険性がある状況を検出します。
  2. runtime·unlock(&runtime·sched);:

    • handoffp関数は、通常、スケジューラのロック(runtime·sched)を保持した状態で実行されます。startm関数を呼び出す前に、このロックを解放する必要があります。これは、startmが新しいMを起動する際に、スケジューラの状態を変更する可能性があるため、デッドロックや競合状態を避けるためです。
  3. startm(p, false);:

    • startm関数は、新しいM(OSスレッド)を起動し、そのMに引数で渡されたPをアタッチしようとします。
    • ここでpを渡すことで、新しく起動されたMは、このPを使ってゴルーチンを実行する準備ができます。
    • 第二引数のfalseは、このMがネットワークポーリング専用ではないことを示唆しますが、このMが最終的にネットワークポーリングを担当するMになる可能性があります。重要なのは、システムに少なくとも1つのアクティブなMが存在し、ネットワークイベントを処理できる状態を確保することです。
  4. return;:

    • 新しいMが起動されたため、現在のhandoffpの処理はここで終了し、Pはアイドルリストには戻されません。代わりに、新しく起動されたMがこのPを引き継ぐか、または別のPが利用可能になるまで待機します。

この変更により、Goランタイムは、すべてのPがアイドル状態になり、ネットワークポーラーも不在という危険な状況を検出し、能動的に新しいMを起動してネットワークポーリングを継続させることで、デッドロックを回避し、システムの応答性を維持します。

関連リンク

参考にした情報源リンク

  • Goのスケジューラに関する公式ドキュメントやブログ記事 (GoのM, P, Gモデルの理解に役立つもの)
  • Goのネットワークポーラーに関する技術解説 (GoのノンブロッキングI/Oの仕組みの理解に役立つもの)
  • デッドロックに関する一般的なコンピュータサイエンスの概念