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

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

このコミットは、GoランタイムにおけるNote同期プリミティブの二重ウェイクアップ(double wakeup)問題を診断し、防止するための変更を導入しています。Noteインターフェースは二重ウェイクアップを禁止しており、このコミットはそれを強制することで、ランタイムの堅牢性と予測可能性を向上させます。

コミット

  • コミットハッシュ: 4380fa6d99284c03e471bafcb1be2db83b225af4
  • Author: Dmitriy Vyukov dvyukov@google.com
  • Date: Mon Dec 24 21:06:57 2012 +0400

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

https://github.com/golang/go/commit/4380fa6d99284c03e471bafcb1be2db83b225af4

元コミット内容

runtime: diagnose double wakeup on Note
Double wakeup is prohibited by the Note interface
and checked in lock_sema.c.

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

変更の背景

GoランタイムのNoteは、ゴルーチン間のシンプルな一回限りの通知メカニズムとして機能します。しかし、このNoteが既にウェイクアップされているにもかかわらず、再度ウェイクアップが試みられる「二重ウェイクアップ」という状況が発生する可能性がありました。Noteインターフェースの設計上、これは禁止されており、lock_sema.c(セマフォベースの実装)では既にチェックが行われていました。

しかし、futex(Fast Userspace Mutexes)ベースの実装であるlock_futex.cでは、この二重ウェイクアップに対する明示的な診断とエラー処理が欠けていました。二重ウェイクアップは、同期プリミティブの誤用やロジックの欠陥を示唆するものであり、未検出のまま放置されると、プログラムの予期せぬ動作やデッドロック、あるいはより深刻な問題を引き起こす可能性があります。

このコミットの目的は、futexベースのNote実装においても二重ウェイクアップを検出し、ランタイムパニック(runtime·throw)を発生させることで、開発者に問題の存在を早期に知らせ、デバッグを容易にすることです。これにより、Noteの利用規約がより厳密に強制され、ランタイムの堅牢性が向上します。

前提知識の解説

GoランタイムのNote

GoランタイムにおけるNoteは、非常に軽量な同期プリミティブであり、主にゴルーチンが単一のイベントを待機し、そのイベントが発生した際に一度だけ通知を受け取るために使用されます。これは、より汎用的なチャネルやsync.Condとは異なり、特定の低レベルな同期シナリオ(例えば、ゴルーチンの起動や終了の通知、GCの同期など)で内部的に利用されます。Noteは「一回限り」の通知メカニズムであり、一度ウェイクアップされると、そのNoteは「セットされた」状態になり、それ以降のウェイクアップ試行は無効であるか、あるいはエラーと見なされるべきです。

Futex (Fast Userspace Mutexes)

Futexは、Linuxカーネルが提供する同期メカニズムであり、主にユーザー空間での高速なミューテックスやセマフォの実装に使用されます。Futexの大きな特徴は、競合が発生しない限りカーネルへのシステムコールを必要とせず、ユーザー空間で同期処理を完結できる点です。これにより、コンテキストスイッチのオーバーヘッドを削減し、高いパフォーマンスを実現します。

Futexは、共有メモリ上の整数変数(key)を介して動作します。

  • FUTEX_WAIT: スレッドはkeyの値が特定の値である間、スリープします。
  • FUTEX_WAKE: スレッドはkeyの値を変更し、待機しているスレッドをウェイクアップします。

Goランタイムは、Linuxシステム上でNoteのような低レベル同期プリミティブを実装するためにFutexを内部的に利用しています。

二重ウェイクアップ(Double Wakeup)とスプリアスウェイクアップ(Spurious Wakeup)

  • 二重ウェイクアップ(Double Wakeup): これは、同期プリミティブ(この場合はNote)が既に「ウェイクアップ済み」の状態であるにもかかわらず、再度ウェイクアップ操作が試みられる状況を指します。Noteの設計思想からすると、これは論理的な誤りであり、通常はプログラムのバグを示唆します。Noteは一回限りの通知を意図しているため、二重ウェイクアップは禁止されています。

  • スプリアスウェイクアップ(Spurious Wakeup): これは、Futexや条件変数などの同期メカニズムにおいて、待機中のスレッドが、実際に通知イベントが発生していないにもかかわらず、カーネルによってウェイクアップされる現象を指します。スプリアスウェイクアップは、同期メカニズムの正常な動作の一部であり、ユーザーコードはウェイクアップされた後に必ず条件を再チェックする必要があります。これはバグではなく、同期プリミティブの設計上の特性です。

このコミットで対処されているのは「二重ウェイクアップ」であり、これはNoteの誤用によるエラー状態です。スプリアスウェイクアップとは根本的に異なる問題です。

技術的詳細

GoランタイムのNoteは、内部的にkeyという整数変数を持っています。このkeyNoteの状態を示します。

  • key0の場合、Noteは「クリア」状態であり、まだウェイクアップされていないことを意味します。
  • key1の場合、Noteは「セット」状態であり、既にウェイクアップされたことを意味します。

runtime·notewakeup関数は、Noteをウェイクアップする役割を担います。この関数が呼び出されると、Notekey0から1にアトミックに設定しようとします。

このコミット以前は、runtime·notewakeupは単にruntime·xchg(&n->key, 1)を実行していました。runtime·xchgはアトミックな交換操作であり、n->keyの現在の値を返し、同時にn->key1に設定します。

問題は、もしn->keyが既に1であった場合(つまり、既にウェイクアップされていた場合)、runtime·xchg1を返しますが、その後の処理ではこの戻り値が利用されていませんでした。そのため、二重ウェイクアップが発生しても、ランタイムはそれを検知せず、エラーとして扱っていませんでした。

このコミットでは、runtime·xchg(&n->key, 1)の戻り値をチェックするロジックが追加されました。

  • もしruntime·xchg(&n->key, 1)1を返した場合、それはn->keyが既に1であったことを意味します。これは二重ウェイクアップの状況であり、Noteインターフェースの規約に違反します。
  • この場合、runtime·throw("notewakeup - double wakeup")が呼び出され、ランタイムパニックが発生します。これにより、開発者は二重ウェイクアップのバグを即座に特定し、修正することができます。

この変更は、Noteの「一回限り」という性質を厳密に強制し、Goランタイムの内部同期メカニズムの信頼性を向上させるものです。

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

--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -111,7 +111,8 @@ runtime·noteclear(Note *n)
 void
 runtime·notewakeup(Note *n)
 {
-\truntime·xchg(&n->key, 1);\n+\tif(runtime·xchg(&n->key, 1))\n+\t\truntime·throw(\"notewakeup - double wakeup\");\n \truntime·futexwakeup(&n->key, 1);\n }\n \n```

## コアとなるコードの解説

変更されたのは`src/pkg/runtime/lock_futex.c`ファイルの`runtime·notewakeup`関数です。

元のコード:
```c
void
runtime·notewakeup(Note *n)
{
	runtime·xchg(&n->key, 1);
	runtime·futexwakeup(&n->key, 1);
}

変更後のコード:

void
runtime·notewakeup(Note *n)
{
	if(runtime·xchg(&n->key, 1))
		runtime·throw("notewakeup - double wakeup");
	runtime·futexwakeup(&n->key, 1);
}
  • runtime·xchg(&n->key, 1): これは、n->keyの値をアトミックに1に設定し、その操作前のn->keyの値を返します。

    • もしn->keyが元々0だった場合(まだウェイクアップされていない状態)、runtime·xchg0を返します。
    • もしn->keyが元々1だった場合(既にウェイクアップされている状態)、runtime·xchg1を返します。
  • if(runtime·xchg(&n->key, 1)): このif文は、runtime·xchgの戻り値を評価します。C言語では、非ゼロの値は真と評価されます。

    • runtime·xchg0を返した場合(正常な初回ウェイクアップ)、if(0)は偽となり、runtime·throwは実行されません。
    • runtime·xchg1を返した場合(二重ウェイクアップ)、if(1)は真となり、runtime·throwが実行されます。
  • runtime·throw("notewakeup - double wakeup"): この関数は、Goランタイムのパニックメカニズムをトリガーします。指定された文字列をエラーメッセージとして、プログラムの実行を停止させます。これにより、二重ウェイクアップという不正な状態が検出された際に、即座にプログラムが異常終了し、開発者が問題の根本原因を特定できるようになります。

  • runtime·futexwakeup(&n->key, 1): この行は変更されていません。n->keyの値を1に設定した後、futexシステムコールを呼び出して、n->keyを待機しているゴルーチンをウェイクアップします。この操作は、key0から1に変わった場合にのみ意味を持ちます。二重ウェイクアップの場合でもこの関数は呼び出されますが、既にウェイクアップされているため、実質的な影響はありません。重要なのは、その前にruntime·throwによって不正な状態が報告されることです。

この変更により、Noteの二重ウェイクアップがランタイムレベルで厳密にチェックされ、開発時のデバッグが大幅に容易になります。

関連リンク

参考にした情報源リンク