慣性聚合 高效追讀感興趣之博客、新聞、科技資訊
閱原文 以慣性聚合開啟

推薦訂閱源

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)
静杀之者:互斥锁、信号量与协程泄漏
amir · 2026-05-25 · via DEV Community

Go 使并发之理显于简易.

尔书之曰:

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

入全屏模式 出全屏模式

倏尔,尔之码行于他之协程.

此简易,乃吾爱 Go 之由。然经于后端系统、通知管道、高流 API、生产服务之实载,吾得要义焉。

Go中多数并发之患,非不用并发所致。

其患乃在未明瓶颈所在,而妄用并发。

或患在锁之阙如。

然甚多时,尤以生产中Go服务为甚,其患适相反:

  • 锁之过甚
  • 持锁过久
  • 网络I/O于关键段落内
  • 永无退出之goroutines
  • 无界goroutines之创生
  • 值拷贝之WaitGroups
  • 无取消策略之channels

是篇,吾欲述所睹之实系统并发之患,吾思之关于互斥锁与信号量,及吾常于其未成生产之变前,何以调试此等问题。


真实之患:偶然成次第之并发

一服务,外观似并发,内犹如单线程之应用。

此常发生于请求流程之大部分,隐于共享一锁之后.

此等模式,较诸众开发者所承,实为常见.

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

入全屏模式 出全屏模式

初观之,或似无虞。

开发者欲护共享之态。此意固善。然锁今所护者,非独共享之存也。实护其全流:

  1. 更一字
  2. 发一函
  3. 叩一库
  4. 或待络之输
  5. 或再试
  6. 或阻他协程久时

此非互斥矣.

此乃交通阻塞也.

凡欲求同锁者,必待全流毕而后可。是故纵使服务有百千协程,大半系统亦成次第之序。

所险者,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阻于网呼之背。

更优之版本,别共享状态之变与缓工。

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}
    }
}

入全景模式 出全屏模式

今请求路径不直系于电子邮件提供者之迟滞。

此乃真解。

非止“用 goroutines”而已。

其修也,在界乎共内存、外设出入、及反压之阈也。


互斥锁非恶,巨要区乃恶。

吾时见开发者畏于互斥锁。

此非其道。

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()

入全景模式 出全屏模式

一佳之批判段落,宜淡然无味。

常应作此三者之一。

  • 阅共享之态
  • 更新共享状态
  • 将共享状态复制至本地变量
  • 易指针
  • 增计数器
  • 增补于受护之片/映射

乃解之。

其后,为之费工于锁外。


机枢之下:互斥锁所赐之物

高论而言,互斥锁予汝互斥之效:一时唯有一协程得入防护之域。

然亦予汝内存序之保。

围棋之记忆模型,解锁之操作,先于后之锁操作于同互斥量同步。实践而言,此即一协程更新共享数据而解锁,他协程后锁同互斥量,可安全观测其更新。

此乃多开发者所忘之部分。

互斥锁非止“阻他协程”,亦在构安全可见之界于协程间。

无此界,异协程或同时而读写同内存,是谓数据竞态。既生数据竞态,则程序不复可自信而推究矣。

此故吾不喜“巧”之无锁代码,非有至理不可用也。

大抵后端之务,非需巧竞。

需明竞。


信号量:制其量,非夺其有

互斥锁者,多关乎共内存之有。

信号量者,关乎其量。

譬如,欲理一万用户,然不欲一时发一万书。

愚者或为之。

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

入全景模式 出全屏模式

此乃危殆,盖因其创无界并流也。

users有万件物,汝造万道协程。若每道协程皆行网络之交,启连接,分内存,候外供,则汝之服务可先困,未困于邮供。

简之,用信号量之式可解此:

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则协程欲入__JHSNS_SEG_1b4e1dfc_139__而永阻。

彼协程今已泄漏矣。

一协程之泄漏,未足为患。

千协程之泄漏,则大碍矣。

更安全之版本,乃用缓冲之信道:

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()
    }
}

入全景模式 出全屏模式

要义所在:

每 goroutine 皆需有出之路。

若不能道明goroutine何以止息,恐将有漏泄之患。


WaitGroup以值传递:小错大影

此误于代码检视中,甚易遺之。

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协程之最著名之Bug也:

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

入全屏模式 出全屏模式

依版本之异、境之殊,若不慎摄循环之变,则恐协程得非其值。

守势之式,犹简明也。

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

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

入全景模式 出全屏模式

纵新版Go语言有所精进,吾仍喜此风格于生产代码,盖因其使变量之所属昭然于读者之目也。

文理通顺之并发,乃可持守之并发也。


吾如何于Go中调试锁竞争

吾若疑有并发之瓶颈,非始以臆测为始。

吾始以度量为之。

启用 pprof

import _ "net/http/pprof"

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

    // start application
}

进入全屏模式 退出全屏模式

乃收 profil:

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

进入全屏模式 退出全屏模式

互斥锁争用,启用互斥锁分析:

runtime.SetMutexProfileFraction(1)

进入全屏模式 退出全屏模式

乃察之。

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

入全景模式 出全景模式

二、察协程之数

协程数渐增,常为阻塞或漏泄之兆.

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

入全景模式 出全景模式

用于生产,当为度之显:

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

入全景模式 出全景模式

倾倒goroutine之栈

事滞时,goroutine之倾倒,若金之贵

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

全屏模式入 全屏模式出

寻多goroutine阻于同行

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

全屏模式入 全屏模式出

若五千协程困于同锁或通道,汝已得瓶颈之所在。

四、于试中用识种之器

go test -race ./...

入全景模式 出全屏模式

赛程检测非免费,汝通常不于生产中运行之,然于持续集成与本地调试,实为至要。


吾之实用法则于Go并发之产

吾撰或审并发Go码时,所循之规如此:

1. 锁须其小

唯护其需之数据而锁之

勿锁请求数生之全境

2. 永勿将迟缓之I/O置入互斥锁内

勿于要害处调用数据库、HTTP、邮件、文件上传及第三方API。

3. 限并

勿创无节制之协程。

宜用工池、信号量、队列或速率限制。

4. 每协程须有歇止之径

context.Context、通道闭或显式取消。

勿复制同步之器

共享时,以指针传 *sync.WaitGroup*sync.Mutex 及相似之器

优化之先,必先度量 pprof

、运行时之度,踪迹、记录,及协程之转储

臆断非调试之实 __JHSNS_SEG_1b4e1dfc_236__尚平淡之并发

至妙之并行代码,往往非巧思所成。

其理明,可量度,且易止息。


终章所思

Go予吾辈强并行之器,然未自予良并行之构。

协程价廉,然非无偿。

互斥锁速,然若持之绕缓务,可损通量。

一通道雅致,然若无人接收,则可泄其协程。

一WaitGroup简易,然若复制之,则可破汝全流。

于我而言,资深之Go工程非在于用尽所有并发原语,而在于知其不用之时,识其真实边界,察其系统负重之态。

尔后若再书此:

mu.Lock()

入全景模式 出全景模式

行前问一问:

吾所护者何,此锁何速可释?

此一问,可救汝服务于无声之生产瓶颈.


参考文献