Spinlocks vs. Mutexes: When to Spin and When to Sleep

2025/12/08 9:38

Spinlocks vs. Mutexes: When to Spin and When to Sleep

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

Summary:

スピンロックは、ロック変数をタイトなループで繰り返しテストするため、同期の中で最も高速です。しかし、ロックが占有されている場合でもCPUサイクルを消費し続けるため、キャッシュラインのコンテストが発生します。一方、ミューテックスは重い競合時に遅くなる傾向があります。これはカーネル支援(futex 呼び出し)に依存し、コンテキストスイッチを引き起こす可能性があるためです。しかし、CPU時間の浪費を避け、リアルタイムシステムで優先度逆転を防止するための優先度継承機能を提供します。どちらを選択するかは、クリティカルセクションの長さと競合頻度に依存します:非常に短い(<100 ns)場合はスピンロックが有利です;中程度(100 ns–10 µs)の場合は、最初にスピンし、必要に応じてミューテックスへフォールバックする適応型またはハイブリッドロックが最適です;長いセクションでは標準ミューテックスを使用すべきです。このガイドラインは、コンテキストスイッチ、キャッシュミス、futex 使用量に関するプロファイルデータで裏付けられており、Redis や PostgreSQL のような高性能システムの開発者がレイテンシとスループットを最適化しつつ、予測可能なリアルタイム動作を維持するのに役立ちます。

Summary Skeleton

What the text is mainly trying to say (main message)
スピンロックは高速ですが消費的であり、ミューテックスは競合時に遅くなるものの高CPU使用率や優先度逆転を回避できるため、クリティカルセクションの長さと競合レベルによって選択が決まります。

Evidence / reasoning (why this is said)

  • スピンロックはタイトな
    CMPXCHG
    ループを使用し、待機中に100 % CPU を消費しキャッシュラインのバウンスを引き起こします。
  • ミューテックスは競合時に futex システムコール(約500 ns)とコンテキストスイッチへフォールバックしますが、非競合時の高速パス読み取り(<50 ns)は安価です。
  • プロファイル指標(コンテキストスイッチ数、キャッシュミス、futex カウント)がトレードオフを確認しています。

Related cases / background (context, past events, surrounding info)

  • Redis は短いセクション(<50 ns)でスピンロックを使用し、PostgreSQL はナノ秒レベルの検索にスピンロック、I/O にはミューテックスを利用します。
  • Linux の PREEMPT_RT カーネルは優先度継承ミューテックスを必要とし、無制限の遅延を防止します。
  • 現代のメモリアロケータは、偽共有を防ぐためにロックオブジェクトを 64 バイトキャッシュラインにパディングしています。

What may happen next (future developments / projections written in the text)
推奨はハイブリッドまたは適応型ロッキング戦略の採用です:<100 ns のセクションにはスピンロック、100 ns–10 µs にはハイブリッド/適応ミューテックス、10 µs 超の場合は通常ミューテックスを使用。リアルタイムシステムは引き続き優先度継承ミューテックスを重視します。

What impacts this could have (users / companies / industry)
正しいロックタイプの選択は CPU 使用率を削減し、レイテンシを向上させ、リアルタイムアプリケーションで優先度逆転を防止します。Redis や PostgreSQL のような高性能サーバー開発者やカーネル作成者がこれらのガイドラインを活用してスループットと応答性を最適化できます。

本文

同期プリミティブ ― スピンする時とスリープする時

perf top
を見ていると、CPU 時間の 60 % が
pthread_mutex_lock
に費やされていることに気づく。
レイテンシが「良い」から「排泄室」に変わってしまう。
誰かが「ただスピンロックを使えばいい」と言ったら、16 コアのサーバは 100 % で無駄な作業をしてしまう。

これが 同期プリミティブの罠 だ。多くのエンジニアは、各プリミティブが実際にどんな場面で意味を持つかを説明されないまま飛び込むので起こる現象である。


実際に何が起きているか

プリミティブ仕組みコンテスト時のコスト一般的な使い道
スピンロックユーザ空間で
LOCK CMPXCHG
(atomic compare‑and‑swap)を繰り返し成功するまでループ。
待機中は 100 % CPU を消費。キャッシュラインのバウンスがあると 40–80 ns 程度かかる。非常に短いクリティカルセクション(< 100 ns)、低コンテスト(2–4 スレッド)。
ミューテックスファーストパスは atomic test‑and‑set。失敗すると
futex(FUTEX_WAIT)
システムコール(約 500 ns)→ コンテキストスイッチ(3–5 µs)。ロック解放時に
futex(FUTEX_WAKE)
が待機者を起こす。
スリープ/ウェイクのオーバーヘッド、スケジューラが関与。中〜長いクリティカルセクション(≥ 100 ns)、中~高コンテスト、CPU を他の作業に回したい場合。

なぜスピンロックは危険なのか

リスク詳細
プリエンプタブルなコンテキストロックを保持しているスレッドがプリエンプトされると、他の全てのスレッドが 1 スライス(Linux では約 100 ms)分だけスピンし続ける。カーネルはスピンロック周辺でプリエンプションを無効化するが、ユーザ空間では不可能。
優先度逆転低優先度のスレッドがスピンロックを保持すると、高優先度のスレッドは永遠にスピンし続ける。PI ミューテックスは保持者の優先度を一時的に上げて回避するが、スピンロックではできない。
偽共有同じキャッシュライン上に無関係な 2 つのロックがあると不要なコンテストが発生する。現代のメモリアロケータは 64 バイト境界でパディング(
alignas(64)
/
__attribute__((aligned(64)))
)している。

適切なプリミティブを選ぶ

クリティカルセクション長コンテストレベル推奨ロック
< 100 ns2–4 スレッドスピンロック(数クロックで解放)
100 ns – 10 µs中程度ハイブリッドミューテックス(glibc の adaptive mutex: 短時間スピン後にスリープ)。PostgreSQL の LWLock も同様。
> 10 µs または高コンテストどれでも通常のミューテックス(スケジューラに任せる)
リアルタイム / 境界付きレイテンシPREEMPT_RT カーネルで PI ミューテックス を使用。スピンロックは除外。

実践的診断

  1. perf stat -e context-switches,cache-misses

    高いコンテキストスイッチ+低 CPU → ミューテックスのオーバーヘッド
    高いキャッシュミス+100 % CPU → ロック競合 / 偽共有

  2. strace -c
    – システムコールをカウント。
    すべての
    futex()
    がコンテスト中のミューテックスを示す。

  3. /proc/PID/status
    – 自発的 vs 非自発的コンテキストスイッチを確認。
    スピンロック保持時に非自発的スイッチがあると警告。


サンプルプログラム

以下は、先ほど説明した挙動を示す 2 つの最小限のテストプログラムです。
コンパイル方法:

gcc -Wall -Wextra -O2 -pthread spinlock_test.c -o spinlock_test
gcc -Wall -Wextra -O2 -pthread mutex_test.c -o mutex_test

spinlock_test.c

#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
#include <time.h>

#define NUM_THREADS 4
#define ITERATIONS 1000000

typedef struct {
    atomic_int lock;
    long counter;
} spinlock_t;

static inline void spinlock_acquire(spinlock_t *s) {
    int expected = 0;
    while (!atomic_compare_exchange_weak(&s->lock, &expected, 1))
        expected = 0;          // 再試行
}

static inline void spinlock_release(spinlock_t *s) {
    atomic_store_explicit(&s->lock, 0, memory_order_release);
}

void *worker(void *arg) {
    spinlock_t *s = (spinlock_t *)arg;
    for (long i = 0; i < ITERATIONS; ++i) {
        spinlock_acquire(s);
        s->counter++;
        /* ~100 ns の作業をシミュレート */
        for (volatile int j = 0; j < 10; ++j);
        spinlock_release(s);
    }
    return NULL;
}

int main(void) {
    pthread_t threads[NUM_THREADS];
    spinlock_t lock = { .lock = ATOMIC_VAR_INIT(0), .counter = 0 };
    struct timespec start, end;

    clock_gettime(CLOCK_MONOTONIC, &start);
    for (int i = 0; i < NUM_THREADS; ++i)
        pthread_create(&threads[i], NULL, worker, &lock);

    for (int i = 0; i < NUM_THREADS; ++i)
        pthread_join(threads[i], NULL);
    clock_gettime(CLOCK_MONOTONIC, &end);

    double elapsed = (end.tv_sec - start.tv_sec) +
                     (end.tv_nsec - start.tv_nsec) / 1e9;

    printf("SPINLOCK Results:\n");
    printf("  Final counter: %ld\n", lock.counter);
    printf("  Time: %.3f seconds\n", elapsed);
    printf("  Ops/sec: %.0f\n",
           (double)(NUM_THREADS * ITERATIONS) / elapsed);
    printf("  CPU usage: 100%% (busy‑waiting)\n");
    return 0;
}

mutex_test.c

#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <time.h>

#define NUM_THREADS 4
#define ITERATIONS 1000000

typedef struct {
    pthread_mutex_t mutex;
    long counter;
} mutex_lock_t;

void *worker(void *arg) {
    mutex_lock_t *m = (mutex_lock_t *)arg;
    for (long i = 0; i < ITERATIONS; ++i) {
        pthread_mutex_lock(&m->mutex);
        m->counter++;
        /* ~100 ns の作業をシミュレート */
        for (volatile int j = 0; j < 10; ++j);
        pthread_mutex_unlock(&m->mutex);
    }
    return NULL;
}

int main(void) {
    pthread_t threads[NUM_THREADS];
    mutex_lock_t lock = { .mutex = PTHREAD_MUTEX_INITIALIZER, .counter = 0 };
    struct timespec start, end;

    clock_gettime(CLOCK_MONOTONIC, &start);
    for (int i = 0; i < NUM_THREADS; ++i)
        pthread_create(&threads[i], NULL, worker, &lock);

    for (int i = 0; i < NUM_THREADS; ++i)
        pthread_join(threads[i], NULL);
    clock_gettime(CLOCK_MONOTONIC, &end);

    double elapsed = (end.tv_sec - start.tv_sec) +
                     (end.tv_nsec - start.tv_nsec) / 1e9;

    printf("MUTEX Results:\n");
    printf("  Final counter: %ld\n", lock.counter);
    printf("  Time: %.3f seconds\n", elapsed);
    printf("  Ops/sec: %.0f\n",
           (double)(NUM_THREADS * ITERATIONS) / elapsed);
    printf("  CPU efficient (threads sleep when blocked)\n");
    pthread_mutex_destroy(&lock.mutex);
    return 0;
}

テストの実行

# スピンロック – 100 % CPU が期待される
./spinlock_test &
top -p $(pgrep spinlock_test)

# ミューテックス – 低い CPU 使用率が期待される。スレッドはブロック時にスリープ。
./mutex_test &
top -p $(pgrep mutex_test)

strace -c
でシステムコールのカウントを確認:

strace -c ./spinlock_test   # futex 呼び出しほぼ無し
strace -c ./mutex_test      # thousands of futex(FUTEX_WAIT/WAKE)

/proc/PID/status
のコンテキストスイッチ統計や
perf stat -e cache-misses,cache-references
でキャッシュ挙動を比較。


結論

項目要点
スピンロックCPU を消費するが、数百ナノ秒程度の極めて短いクリティカルセクションに最適。低コンテストでのみ使用すべき。
ミューテックスシステムコールとコンテキストスイッチを伴うオーバーヘッドがあるが、CPU を他の作業に回せる。クリティカルセクションが数クロック以上の場合はデフォルトで選ぶべき。
ハイブリッド / アダプティブミューテックス短時間スピンしてからスリープへ移行するので、平均的な長さのセクションに有効。
PI ミューテックス(PREEMPT_RT カーネル)境界付きレイテンシが重要なリアルタイム環境で必要。スピンロックは不適切。

まず

perf
strace
、/proc の統計を使って測定し、保持時間・コンテストレベル・リアルタイム要件に合わせてプリミティブを選択することが最良のアプローチである。Redis では小さなスピンロックが使用され、PostgreSQL は I/O のためにミューテックスを使っているように、実際のプロダクションシステムもこの考え方に従って設計されている。

同じ日のほかのニュース

一覧に戻る →

2025/12/08 2:18

I failed to recreate the 1996 Space Jam website with Claude

## Japanese Translation: ## 要約 著者は、Claude AI を使って 1996 年の Warner Bros の「Space Jam」ランディングページをスクリーンショットとアセットフォルダから再構築しようとしました。元のサイトは 200 KB 未満の単一 HTML ファイルで、絶対位置決め、テーブルレイアウト、およびタイル状の星空 GIF 背景に依存しています。 **プロセスと所見** 1. **初期試行:** Claude は概算レイアウトを生成しましたが、惑星軌道を誤った位置に配置しました。軌道パターンは認識できたものの、それを再現することには失敗しました。 2. **構造化プロンプト:** 著者は Claude に「知覚分析」「空間解釈」「再構築計画」の各セクションで理由を説明させ、正確なピクセル座標を要求しましたが、Claude はそれらを提供できませんでした。 3. **カスタムツール:** 精度向上のために 50 px → 5 px のグリッドオーバーレイ、ラベル付き座標参照点、色差比較、スクリーンショットサイドバイサイドビューア、およびスクリーンショットを 6 区域に分割するスクリプトを構築しました。 4. **結果:** Claude の調整は目標から 5–10 px 内に留まりましたが、正しい軌道半径(約 350–400 px)には決して収束しませんでした。内部レイアウトが生成されると、その後のフィードバックは元のスクリーンショットではなく、この誤ったモデルに基づいて行われました。 5. **トークナイズ仮説:** 著者は Claude が 16×16 パッチで画像をトークナイズしているため、細かい視覚的粒度が欠如し、セマンティック理解はあるもののピクセル精度が低いと考えました。 6. **ズームインテスト:** 200 % に拡大したスクリーンショットを提供して、大きなパッチで解像度が向上するか確認しましたが、Claude は依然として比例スケーリング指示に従いませんでした。 **結論** このタスクは未解決のままです。実験は Claude の空間推論限界をベンチマークとし、ピクセル単位で正確な画像再構築におけるモデルの現在の制約を示しています。

2025/12/08 7:18

How I block all online ads

## Japanese Translation: > **概要:** > 著者は、ウェブブラウザとモバイルアプリの両方で広告を排除するために長期的かつ多層的なアプローチを説明しています。彼は **Firefox + uBlock Origin** と最小限のフィルタリスト(組み込みのuBlockフィルタ、EasyList、AdGuard – Ads)と「広告でない不快要素」のためのカスタム非広告フィルタを使用します。 > DNS フィルタリングには **Pi‑hole(または AdGuard Home)** を Docker 上で $5 の DigitalOcean ドロップレットに稼働させ、WireGuard VPN の DNS サーバとして設定しています。トラフィックは **クラウドベースの VPN**(DigitalOcean、Hetzner、Azure、Google Cloud、または AWS)を経由し、プラットフォームが公的クラウド IP を検知して広告配信を減らします。 > この設定では **Cloudflare のキャプチャや HTTP エラー** が発生する場合があるため、著者は該当サイトで VPN を無効化しています。また、**Consent‑O‑Matic**(クッキーポップアップ)、**Buster**(キャプチャ)、**SponsorBlock**(動画広告)などのブラウザ拡張機能を推奨します。iOS では **Background App Refresh** をオフにするとデータ収集が減少し、Android では **ReVanced がアプリをパッチできますが、セキュリティリスクがあります** と指摘しています。 > 著者はこの統合戦略を 3 年以上使用しており、現在ほとんど広告を見ることはありません。プラットフォーム別の効果は異なります:YouTube は uBlock Origin + VPN(1週間〜1か月)が必要;Instagram は uBlock Origin のみで十分;Twitch は主に VPN に依存し、数日で効果が現れます;TikTok は両方のツールを使用しますが、数時間だけです。**AdMob** を利用するアプリも DNS ブロックの恩恵を受けます。 > 広告配信ネットワークは数日から数週間でパターンを観察し調整する可能性があるため、継続的な監視が必要です。著者は **Firebog** をブロックリストの良い情報源として引用し、正当なサイトを壊さないように許可リスト(allowlist)を維持する重要性を強調しています。

2025/12/07 23:37

Dollar-stores overcharge cash-strapped customers while promising low prices

## Japanese Translation: ドルジェネラルとファミリードラーは、棚に貼られたタグの価格よりも高い価格で顧客を頻繁に請求し、低所得層の買い物客に不釣り合いな過剰課金が広く発生しています。州検査と独立調査では、一部店舗でエラー率が88%に達するケースや、両チェーン全体で価格設定失敗が一貫して報告されています。 主な例としては、ノースカロライナ州ウィンザーのファミリードラーで23%のスキャンアイテムが過剰請求(同店の4回連続失敗)、オハイオ州ハミルトンのドルジェネラルで76%のエラー率(2022年10月)、ニュージャージー州バウンドブルックのファミリードラーで68%の不一致(2023年2月)があります。2022年1月以降、ドルジェネラルは4,300件以上、ファミリードラーは2,100件以上の価格失敗事例を記録しています。 アリゾナ州(60万ドル)、コロラド州(40万ドル)、ニュージャージー州・バーモント州・ウィスコンシン州・オハイオ州(最大100万ドル)など複数の州がチェーンと訴訟を和解し、連邦および州の司法長官は追加訴訟を提起しています。株主訴訟では、経営陣がシステム的問題を認識していたと主張されています。ニュージャージー州の連邦裁判所は、モバイルアプリ利用に関連する仲裁条項を理由にドルジェネラルに対する集団訴訟を停止し、消費者の救済手段を制限しました。 規制当局は現在の1検査あたり5,000ドル上限を超えるより厳格な執行や高い罰則を課すことができ、さらに州が調査を進めるにつれて追加の和解が生じる可能性があります。影響としては顧客信頼の低下、チェーンへの潜在的財務損失、評判へのダメージ、およびドルストア業界全体での価格設定と人員管理の強化への動きが挙げられます。

Spinlocks vs. Mutexes: When to Spin and When to Sleep | そっか~ニュース