慣性聚合 関心のあるブログ、ニュース、テクノロジーを効率的に追跡
原文を読む 慣性聚合で開く

おすすめ購読元

Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
博客园 - 叶小钗
MyScale Blog
MyScale Blog
V
Visual Studio Blog
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
I
InfoQ
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
Last Week in AI
Last Week in AI
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
博客园 - Franky

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python)
Go並行処理の静かな殺し屋:Mutex、セマフォ、およびGoroutineのリーク
amir · 2026-05-25 · via DEV Community

Goは並行処理を簡単に見せます.

あなたはこう書きます.

go func() {
    // do something concurrently
}()

フルスクリーンモードに入る フルスクリーンモードから退出する

そして突然、あなたのコードが別のgoroutineで実行されます.

そのシンプルさは、私がGoをとても好きな理由の一つです。しかし、バックエンドシステム、通知パイプライン、高トラフィックAPI、実際の負荷での生産サービスを扱ってから、私は重要なことを学びました:

Goにおける多くの並行処理の問題は、並行処理を使用しないから生じるものではありません。

それらは、実際のボトルネックがどこにあるかを理解せずに並行処理を使うことから来ています。

時々問題はロックが欠けている場合があります。

しかし、特にプロダクションのGoサービスでは、問題は逆の場合が多いです:

  • ロックが多すぎます
  • 長時間ロックが保持されています
  • クリティカルセクション内のネットワークI/O
  • 終了しないgoroutines
  • 無制限なgoroutineの作成
  • 値渡しでコピーされたWaitGroups
  • キャンセル戦略なしで使用されるチャネル

本記事では、実際のシステムで見た並行性の問題、メルクセスとセマフォについての私の考察方法、そしてこれらの問題が生産環境のインシデントになる前に通常どのようにデバッグするかを説明したいと思います.


本当の問題:意図せず順次処理になる並行性

サービスは外部から並行的に見えるけれども、内部では単一スレッドのアプリケーションとして振る舞うことがあります。

これは通常、リクエストフローの大部分が一つの共有ロックの背後に隠されている時に起こります

このようなパターンは多くの開発者が認めないほど一般的です

mu.Lock()
user.Name = "Test User"
sendEmail(user)
callDatabase(user)
mu.Unlock()

フルスクリーンモードに入る フルスクリーンモードから抜ける

最初は安全に見えるかもしれません

開発者は共有状態を保護しようとしていた。その部分は合理的だ。しかし、今のロックは共有メモリだけでなくもっと多くを保護している。それは全てのフローを保護している:

  1. フィールドを更新
  2. メールを送信
  3. データベースを呼び出す
  4. おそらくネットワークI/Oの待機
  5. おそらく再試行
  6. おそらく長い時間で他のgoroutinesをブロック

それはもはや互換キーではありません.

それは交通渋滞です.

同じロックが必要なすべてのゴルーチンは、全体のフローが終わるまで待つ必要があります。したがって、あなたのサービスが数百や数千のゴルーチンを持っていても、システムの大部分が順次処理になります。

危険なのは、CPU使用率がまだ正常に見えたり、低く見えたりすることです。メモリも正常に見えるかもしれません。しかし、遅延が増加し、スループットが低下し、p95/p99の応答時間が不安定になります。

これが、ロック競合が基本的なインフラメトリクスだけでは時々見つからにくい理由です。


プロダクションスタイルの例:ミューテックス内のメール

ユーザーステートを更新し、通知を送信するサービスがあると想像してみてください.

type Service struct {
    mu    sync.Mutex
    state map[int]string
}

func (s *Service) ProcessUsers(users []User) {
    s.mu.Lock()
    defer s.mu.Unlock()

    for _, user := range users {
        s.state[user.ID] = "processed"
        sendEmail(user) // slow network I/O inside the lock
    }
}

フルスクリーンモードに入る フルスクリーンモードから退出する

このコードはデータレースの観点から安全です.

しかし、パフォーマンスの観点からは危険です.

ミューテックスは可能な限り小さな共有メモリ操作を保護すべきです。遅い外部作業(例えば:)を保護すべきではありません。

  • メールの送信
  • 別のマイクロサービスを呼び出している
  • データベースクエリ
  • HTTPリクエスト
  • ファイルアップロード
  • 遅い外部シンクにログを記録
  • 第三者APIの待機

メモリの更新はナノ秒またはマイクロ秒かかる可能性があります。メールの呼び出しはミリ秒または秒かかる可能性があります

その違いは重要です

ロックが保持されている間にsendEmailが実行されると、s.muが必要な他のgoroutineはネットワーク呼び出しの後にブロックされます

より良いバージョンでは、共有状態の変更を遅い作業から分離します

func (s *Service) ProcessUsers(users []User) {
    emails := make([]User, 0, len(users))

    s.mu.Lock()
    for _, user := range users {
        s.state[user.ID] = "processed"
        emails = append(emails, user)
    }
    s.mu.Unlock()

    for _, user := range emails {
        sendEmail(user)
    }
}

フルスクリーンモードに入る フルスクリーンモードから退出する

これも改善されていますなぜならロックは共有マップのみを保護するからです

実際の生産環境では、私は通常遅い作業をキューや制限付きワーカープールにプッシュすることを好みます:

func (s *Service) ProcessUsers(users []User, jobs chan<- EmailJob) {
    s.mu.Lock()
    for _, user := range users {
        s.state[user.ID] = "processed"
    }
    s.mu.Unlock()

    for _, user := range users {
        jobs <- EmailJob{UserID: user.ID, Email: user.Email}
    }
}

全画面モードに入る 全画面モードから退出する

今ではリクエストパスが直接メールプロバイダーの遅延に依存しません

これが本当の修正です

「ゴルーチンを使う」だけではありません

共有メモリ、外部I/O、バックプレッシャーとの境界を設計することで解決策が設計されています


ミューテックスは悪くありません。大きなクリティカルセクションは悪いです。

私は時々開発者がミューテックスを恐れるのを見ます

それは間違った教訓です

sync.Mutexは正しく使えば単純で速く、完璧です。問題はミューテックスではなく、クリティカルセクションのサイズです。

これを心に留めておくべきです:

mu.Lock()
// only touch shared memory here
mu.Unlock()

フルスクリーンモードに入る フルスクリーンモードから退出する

これではありません:

mu.Lock()
// shared memory
// database call
// HTTP call
// email call
// JSON encoding
// logging
// metrics push
mu.Unlock()

フルスクリーンモードに入る フルスクリーンモードから退出する

良いクリティカルセクションは退屈であるべきです

通常、これらのいずれかを行うべきです

  • 共有状態を読み取る
  • 共有状態を更新する
  • ローカル変数に共有状態をコピーします
  • ポインタを交換します
  • カウンターをインクリメントします
  • 保護されたスライス/マップに追加します

その後、ロックを解除します

その後、ロック外で高コストの作業を行います


ドライブ下:ミューテックスがあなたに提供するもの

高レベルでは、互斥ロックは排他性を提供します:一度に一つのゴルーチンだけが保護されたセクションに入ることができます

しかし、メモリ順序の保証も提供します。

Goのメモリモデルでは、ロック操作の前に同じmutexの後のロック操作でアンロック操作が同期します。実用的には、これは一つのgoroutineが共有データを更新してアンロックした場合、後で同じmutexをロックする別のgoroutineがその更新を安全に観察できることを意味します。

それが多くの開発者が忘れる部分です。

互排 locks は「他の goroutines をブロックすること」だけでなく、「goroutines の間の安全な可視性境界を作る」ことも意味します。

その境界がないと、異なる goroutines が同じ時刻に同じメモリを読み書きし、それでデータ競合が発生します。データ競合が発生すると、あなたのプログラムは自信を持って考えることができないものになります。

それが why I do not like “clever” lock-free code unless there is a very strong reason for it.

Most backend services do not need clever concurrency.

They need clear concurrency.


セマフォ:キャパシティを制御する、所有権ではない

互換メカニズムは通常、共有メモリの所有権についてです.

セマフォはキャパシティについてです。

例えば、10,000人のユーザーを処理したいが、同時に10,000件のメールを送信したくないとしよう。

単純なバージョンでは、こんなことをするかもしれない:

for _, user := range users {
    go sendEmail(user)
}

フルスクリーンモードに入る フルスクリーンモードから退出する

これは危険なため、無制限の並行性が生じる。

usersには10,000のアイテムがあり、あなたは10,000のgoroutineを作成します。各goroutineがネットワークI/Oを実行し、接続を開き、メモリを割り当て、外部プロバイダーに待機すると、メールプロバイダーを過負荷にする前にあなた自身のサービスを過負荷にすることができます。

シンプルなセマフォパターンでこれを修正できます:

sem := make(chan struct{}, 20) // allow only 20 concurrent email sends
var wg sync.WaitGroup

for _, user := range users {
    user := user

    sem <- struct{}{}
    wg.Add(1)

    go func() {
        defer wg.Done()
        defer func() { <-sem }()

        sendEmail(user)
    }()
}

wg.Wait()

フルスクリーンモードに入る フルスクリーンモードを出る

現在コードはまだ並行処理を使用していますが、並行処理は制限されています.

その詳細は本番環境では非常に重要です.

無制限の並行処理は拡張性がありません.

それは遅延された失敗です.


本番環境コードのためのより良いワーカープール

セマフォパターンは便利ですが、継続的に実行されるサービスの場合、私はよくワーカープールを好みます.

type EmailJob struct {
    UserID int
    Email  string
}

func startEmailWorkers(ctx context.Context, workerCount int, jobs <-chan EmailJob) {
    var wg sync.WaitGroup

    for i := 0; i < workerCount; i++ {
        wg.Add(1)

        go func(workerID int) {
            defer wg.Done()

            for {
                select {
                case <-ctx.Done():
                    return

                case job, ok := <-jobs:
                    if !ok {
                        return
                    }

                    if err := sendEmailJob(ctx, job); err != nil {
                        // In real systems: log, retry, dead-letter, or expose metrics.
                        fmt.Printf("worker=%d failed to send email user_id=%d err=%v\n", workerID, job.UserID, err)
                    }
                }
            }
        }(i)
    }

    go func() {
        wg.Wait()
    }()
}

フルスクリーンモードを入れる フルスクリーンモードを退出する

これにより、より優れた運用制御が可能になります:

  • 固定された並行処理
  • 簡単なメトリクス
  • 簡単なシャットダウン
  • 簡単なリトライ戦略
  • 簡単なバックプレッシャー
  • 簡単なレートリミッティング

「私はゴルーチンを使った」と「私は並行システムを設計した」という違いです


ゴルーチンリーク:すぐに爆発しないバグ

ゴルーチンリークはGoにおける最も一般的な生産問題の一つです

それは危険なため、サービスがすぐにクラッシュしない可能性があります。数時間や数日をかけて徐々に悪化する可能性があります

ここに典型的な例があります

func process() error {
    ch := make(chan result)

    go func() {
        ch <- heavyComputation()
    }()

    select {
    case res := <-ch:
        return handle(res)

    case <-time.After(1 * time.Second):
        return errors.New("timeout")
    }
}

フルスクリーンモードを開始 フルスクリーンモードを終了

問題は微妙です

chはバッファされていません

タイムアウトが最初に発生した場合、processが返されます。その後、chを待つ受信者がいません

heavyComputation()が終了すると、ゴルーチンはchに送信しようとし、永遠にブロックされます。

そのゴルーチンはリークしています.

一つのリークしたゴルーチンは問題ではありません.

数千のリークしたゴルーチンは問題です.

より安全なバージョンはバッファ付きチャネルを使用します.

func process() error {
    ch := make(chan result, 1)

    go func() {
        ch <- heavyComputation()
    }()

    select {
    case res := <-ch:
        return handle(res)

    case <-time.After(1 * time.Second):
        return errors.New("timeout")
    }
}

フルスクリーンモードに入る フルスクリーンモードから退出する

これはタイムアウト後にもゴルーチンが送信でブロックされるのを防ぎます.

実際のサービスでは、コンテキストベースのキャンセルを好みます:

func process(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    ch := make(chan result, 1)

    go func() {
        res := heavyComputation(ctx)

        select {
        case ch <- res:
        case <-ctx.Done():
        }
    }()

    select {
    case res := <-ch:
        return handle(res)

    case <-ctx.Done():
        return ctx.Err()
    }
}

フルスクリーンモードを開始 フルスクリーンモードを終了

重要な教訓:

各ゴルーチンには終了経路が必要です。

ゴルーチンがどのように停止するかを説明できない場合、おそらく漏れが発生する可能性があります。


WaitGroup by Value: 小さなミスが大きな影響を与える

コードレビューで非常に見落としやすいミスです:

func worker(wg sync.WaitGroup) { // wrong: copied by value
    defer wg.Done()

    // do work
}

フルスクリーンモードを開始 フルスクリーンモードを終了

sync.WaitGroupは、初めて使用した後にコピーしないでください。

値渡しすると、その内部状態がコピーされます。ワーカーはコピーに対してDone()を呼び出しますが、メインゴルーチンが待っている元のWaitGroupではありません。

これにより、デッドロックが発生する可能性があります。

正しいバージョン:

func worker(wg *sync.WaitGroup) {
    defer wg.Done()

    // do work
}

フルスクリーンモードを入力 フルスクリーンモードを退出

そして使用法:

var wg sync.WaitGroup

for i := 0; i < 10; i++ {
    wg.Add(1)
    go worker(&wg)
}

wg.Wait()

フルスクリーンモードを入力 フルスクリーンモードを退出

このルールは他の同期原語(例えばsync.Mutex)にも適用されます.

初めて使用した後はコピーしないでください.


ループ変数の罠

これはかつてGoの並行処理における最も有名なバグの一つでした:

for _, user := range users {
    go func() {
        sendEmail(user)
    }()
}

フルスクリーンモードに入る フルスクリーンモードから退出する

Goのバージョンや文脈によっては、ループ変数を誤ってキャプチャするとgoroutinesが間違った値を使用する可能性があります

防御的なパターンは依然としてシンプルで明確です:

for _, user := range users {
    user := user

    go func() {
        sendEmail(user)
    }()
}

フルスクリーンモードに入る フルスクリーンモードから退出する

新しいGoバージョンで改善されたとしても、本番コードでこのスタイルを好む理由は、変数の所有権を読者が明確に理解できるからです.

読みやすい並行処理は保守しやすい並行処理です.


Goでロックの競合をデバッグする方法

並行処理のボトルネックを疑うと、まず推測を始めません.

まず測定を始めます。

1. pprofを有効にする

import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // start application
}

フルスクリーンモードを開始 フルスクリーンモードを終了

次に、プロファイルを収集します:

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

フルスクリーンモードを開始 フルスクリーンモードを終了

互斥ロックの競合に関しては、互斥ロックのプロファイリングを有効にします:

runtime.SetMutexProfileFraction(1)

フルスクリーンモードを開始 フルスクリーンモードを終了

次に、確認します

go tool pprof http://localhost:6060/debug/pprof/mutex

フルスクリーンモードを開始 フルスクリーンモードを終了

2. ゴルーチンの数を確認

ゴルーチンの数が増加することは、ブロックされたゴルーチンやリークのサインであることが多い

fmt.Println("goroutines:", runtime.NumGoroutine())

フルスクリーンモードを開始 フルスクリーンモードを終了

本番環境では、メトリクスとして公開する

prometheus.NewGaugeFunc(
    prometheus.GaugeOpts{
        Name: "go_goroutines_current",
        Help: "Current number of goroutines.",
    },
    func() float64 {
        return float64(runtime.NumGoroutine())
    },
)

フルスクリーンモードを開始 フルスクリーンモードを終了

3. ゴルーチンのスタックダンプ

サービスが固着している場合、ゴルーチンダンプは貴重です。

curl http://localhost:6060/debug/pprof/goroutine?debug=2

フルスクリーンモードに入る フルスクリーンモードから退出する

同じ行で多くのゴルーチンがブロックされているかを探す:

sync.(*Mutex).Lock
chan send
chan receive
net/http.(*Transport).RoundTrip

フルスクリーンモードに入る フルスクリーンモードから退出する

5,000個のgoroutineが同じロックやチャネルでブロックされている場合、ボトルネックを発見しました

4. テストでレース検出器を使用

go test -race ./...

フルスクリーンモードに入る フルスクリーンモードから退出する

レース検出器は無料ではありませんが、通常は本番環境で実行しませんが、CIおよびローカルデバッグでは非常に有用です。


私の生産的なGo並行処理のための実用的なルール

これらは、並行Goコードを書いたりレビューしたりする際に守ろうとするルールです:

1. ロックは小さく保ちなさい

保護が必要なデータのみをロックします.

リクエストのライフサイクル全体をロックしないでください.

2. ミューテックス内に遅いI/Oを置かないでください

クリティカルセクション内でデータベース呼び出し、HTTP呼び出し、メール送信、ファイルアップロード、サードパーティAPI呼び出しを避ける.

3. バウンドコンカレンシー

無制限のgoroutineを作成しない.

ワーカーポール、セマフォ、キュー、またはレートリミッターを使用する.

4. 各goroutineにはシャットダウン経路が必要

context.Context、チャネルクローズ、または明示的なキャンセルを使用する.

5. 同期化プリミティブをコピーしない

共有する際は、*sync.WaitGroup*sync.Mutex、および類似のプリミティブをポインタで渡す

6. 最適化する前に測定する

pprof、実行時メトリクス、トレース、ログ、およびゴルーチンダンプを使用する

推測はデバッグではない

7. 嫌な並行性を好む

最良質な並行コードは通常賢いものではない

明確で測定可能で、停止しやすくなっている


最終的な考え

Goは私たちに強力な並行処理ツールを与えてくれますが、それが自動的に良い並行設計を与えてくれるわけではありません

ゴルーチンは安価ですが、無料ではありません

ミューテックスは速いですが、遅い作業の周りで保持しているとスループットを壊すことがあります

チャンネルは洗練されているが、誰も受信していない場合にゴルーチンがリークする可能性がある。

WaitGroupはシンプルだが、コピーすると全体的なフローが壊れる可能性がある。

私にとって、上級のGoエンジニアリングはすべての並行性原語を使うことではなく、それらを使わない時期、本当の境界線がどこか、そして負荷があるときにシステムがどのように振る舞うかを知ることである。

次回あなたがこれを書くとき

mu.Lock()

フルスクリーンモードを開始 フルスクリーンモードを終了

進む前に一つ質問する:

私は何を保護しているのか、そしてこのロックをどれくらいの速さで解放できるのか

その一つの質問で、あなたのサービスを静かな生産のボトルネックから救うことができる


参考文献