atomic 原子操作

C++ 的 std::atomic 在 Go 里是 sync/atomic 包:无锁计数器、无锁标志位、无锁状态机的最优解 一、简述 sync/atomic 提供对内存地址的原子操作:Add(原子加)、Load(原子读)、Store(原子写)、CompareAndSwap(比较并交换)、Swap(交换)。它们作用于 int32 / int64 / uint32 / uint64 / uintptr / unsafe.Pointer 以及 atomic.Value。 和 Mutex 相比,原子操作是免锁(lock-free)的:既不阻塞、也无上下文切换开销,在「高频计数器、状态标志、一次性初始化标志」这类场景下性能明显优于锁。代价是只能保护单变量,且不能声明「一段代码的整齐」。 核心要点:读——Load、写——Store、改——Add / CompareAndSwap。把「多步的读-改-写」用一个 CAS 原子完成,是理解 Go 原子操作的钥匙。C++ 里的 std::atomic<int> 心智模型几乎原样平移,差异只在 API 命名和哪些类型支持。 二、C++ 对照速查表 概念 C++ Go 关键差异 原子整数 std::atomic<int> n{0} var n atomic.Int64(或 var n int64 + atomic.AddInt64) Go 1.19+ 有类型化 atomic.Int64,更顺手 原子加 n.fetch_add(1) atomic.AddInt64(&n, 1) Go 返回新值;AddInt32 / AddUint64 等按类型 原子读 n.load() atomic.LoadInt64(&n) Go 读不写也有可能被优化异常,必须用 Load 原子写 n.store(42) atomic.StoreInt64(&n, 42) Go 生硬但显式;Load/Store 配对使用 比较并交换 n.compare_exchange_strong(expected, desired) atomic.CompareAndSwapInt64(&n, old, new) CAS 失败返回 false,循环重试是惯用法 原子交换 n.exchange(42) atomic.SwapInt64(&n, 42) 有返回:交换前旧值 原子布尔/指针 std::atomic<bool> / std::atomic<T*> atomic.Bool(1.19+)/ atomic.Pointer[T](1.19+) Go 类型化版本避免手写 unsafe.Pointer 任意值原子存取 std::mutex 包 struct atomic.Value(存储任意类型) Value 的 Load 要求「上次存过的类型」 内存顺序 memory_order_relaxed / seq_cst Go 不暴露 memory_order,原子操作只有一种语义 自 Go 1.19 起内存模型正式规定:原子访问之间顺序一致(sequentially consistent) 三、逐主题详解 3.1 计数器:AddInt64 / AddUint64 经典场景——无锁计数器,多 goroutine 并发累加。 ...

2026 年 10 月 13 日

channel 详解

C++ 里你得自己造「互斥锁 + 条件变量 + 队列」才能做到的线程通信,Go 用一个 channel 就把它变成了语言级原语 一、简述 channel(通道)是 Go 并发模型的核心,它的口号是: 「Do not communicate by sharing memory; instead, share memory by communicating.」 —— 不要通过共享内存来通信,而要通过通信来共享内存。 一个 channel 就是一个带类型的 FIFO 队列,goroutine 之间通过它发送 / 接收数据,同时天然完成了同步(谁等谁、什么时候等)与数据传递两件事。 作为 C++ 开发者,你过去写线程同步大概是这样:std::mutex 保护共享数据 + std::condition_variable 通知 + std::queue 存数据 + 各种 unique_lock。而 Go 把这三件套打包成了一个 channel:发送阻塞、接收阻塞、容量限制、关闭通知全部内建,而且它在语言层面保证了 channel 操作本身的线程安全。 本笔记覆盖:创建、无缓冲 / 有缓冲的阻塞语义、单向 channel、关闭与 range 消费、nil channel、死锁场景,以及「channel 当锁」的奇技淫巧。 二、C++ 对照速查表 概念 C++ Go 关键差异 消息队列 std::queue<T> + std::mutex + std::condition_variable ch := make(chan T) Go 一条语句搞定,且是语言级原语 无缓冲阻塞队列 手写「空/满都等条件变量」的队列 make(chan T)(默认无缓冲) 无缓冲 channel 天然同步:发=等收 有缓冲队列 std::queue + 信号量限制 make(chan T, 5) 缓冲即容量,满则发送阻塞 发送操作 queue.push(x); cv.notify_one(); ch <- x <- 方向右:发送 接收操作 queue.pop();(空时等 cv) x := <-ch <- 方向左:接收 队列空时接收 条件变量等待 + 谓词循环 阻塞直到有值 Go 不需要手写谓词循环 队列满时发送 信号量 / 容量检查 阻塞直到有空位 缓冲满才阻塞,缓冲空才阻塞 生命周期管理 队列对象手动析构、小心悬垂 close(ch),只能发送方关闭 关闭后 range 自动结束 多读多写 一把锁 + 一个队列满危险 内建线程安全,任意并发收发 Go 收发都允许并发,无需额外锁 非阻塞操作 try_lock / 检查队列非空 select + default(下篇讲) select 是非阻塞收发的标准姿势 三、逐主题详解 3.1 创建 channel:make package main import "fmt" func main() { // 无缓冲 channel:接收方和发送方必须同时准备好,相当于一次「握手」 ch1 := make(chan int) // 有缓冲 channel:容量为 5,缓冲不满时发送不阻塞 ch2 := make(chan string, 5) // 只声明不 make 的 channel 是 nil channel var ch3 chan float64 fmt.Println(ch3 == nil) // true(nil channel 不能收发,见 3.7) fmt.Println(ch1 == nil, ch2 == nil) // false, false _ = ch1 _ = ch2 } ⚠️ C++ 对照:make(chan int) 既有「分配」又有「初始化」两层含义,像 std::make_shared<T>()。注意这里返回的是 channel 本身,不是一个指针——但你如果 ch2 := make(chan int) 再 ch2 = ch2,传递的都是同一个底层通道的引用,多个 goroutine 共享它时不需要再加锁。 ...

2026 年 10 月 13 日

context 超时取消与传值

C++ 没有直接对应的东西——它最接近 std::jthread 的 stop_token:一个穿过调用链的「取消令牌 + 截止时间 + 键值袋」 一、简述 context.Context(标准库 context 包)是 Go 并发中传播取消信号、截止时间与请求级数据的载体。它贯穿整个标准库(HTTP 请求、数据库、RPC),是每一个「可能被取消的耗时代码」的标配第一参数。 它解决 C++ 里三个长期痛点: 传递取消信号:深调用链中,任何一层想取消整条链,怎么办?C++ 用 std::atomic<bool> 到处传 + 检查,或 std::jthread 的 stop_token。Go 里就是 ctx.Done() + 广播。 传递截止时间:每个函数都检查「现在是不是超时了」很烦。Go 里 context.WithDeadline 一路带过去,子任务自己查 ctx.Done()。 传递请求级数据:分布式的 trace id、用户信息等「横切数据」,Go 用 WithValue 携带,避免每个函数都加参。 核心要点:context.Context 只做三件事:取消(Cancel)、截止(Deadline/Timeout)、传值(Value)。 它不是「任意全局状态袋」,不该往里塞业务对象;也不是给每个函数都强加的仪式,而是给「可能被取消的耗时代码」用的。 二、C++ 对照速查表 概念 C++ Go 关键差异 取消令牌 std::stop_token / 手写 atomic<bool> ctx.Done() + 树形传播 Go 一次 cancel 广播整棵子树 超时取消 std::stoptoken + 手算时间 context.WithTimeout(ctx, 3*time.Second) 一行完成「到时自动 cancel」 截止时间 steady_clock::now() + d 自己算 context.WithDeadline(ctx, t) Go 语义是绝对时刻 手动取消 手写 atomic<bool> + 到处检查 cancelFunc()(WithCancel 返回) cancel 是「广播」,不只本层 关联请求数据 线程局部 / 全局 map(危险) context.WithValue(parent, key, val) Go 类型安全 + 随 ctx 走 goroutine 收尾 jthread.request_stop() + join select { case <-ctx.Done(): ... } Go 惯例:非主路径用 select 监听取消 取消原因 无 ctx.Err() → context.Canceled / DeadlineExceeded 区分「被取消」与「超时」 上下文传播 无(要么传 token,要么全局) 函数第一参数约定 Go 靠约定 + vet 检查不传 nil 三、逐主题详解 3.1 两个根:context.Background() 与 context.TODO() package main import ( "context" "fmt" ) func main() { // Background:程序的「根上下文」,所有 context 树的起点 ctx := context.Background() fmt.Println("Background Err =", ctx.Err()) // nil(永远不会被取消) fmt.Println("Background Done =", ctx.Done()) // nil channel(永不关闭) // TODO:还没想好用哪个 context 时的占位,语义同 Background todo := context.TODO() fmt.Println("TODO 等价于 Background:", todo.Err() == nil) _ = ctx _ = todo } 规则: ...

2026 年 10 月 13 日

goroutine

Go 并发的基本执行单元:比线程轻量十倍的绿色协程,由 Go 运行时调度 一、C++ 对照速查表 概念 C++ Go 关键差异 并发执行单元 std::thread go func() goroutine 由运行时管理,不映射到 OS 线程 创建成本 ~1MB 栈 + 系统调用 ~2KB 栈,动态增长 goroutine 创建成本极低,可轻松创建上万 启动方式 std::thread t(func); t.join(); go func() Go 无需手动 join,用 channel/WaitGroup 同步 调度模型 1:1 映射 OS 线程 M:N 调度(GMP 模型) 运行时把 N 个 goroutine 调度到 M 个线程上 栈大小 固定(通常 1~8MB) 初始 2KB,按需增长/收缩 同一地址空间下可容纳海量 goroutine 终止方式 thread 析构或 detach 主 goroutine 结束则全部退出 goroutine 是 “fire and forget” 核心要点:goroutine 是 Go 并发的基石。创建一个 goroutine 只用一个 go 关键字,运行时自动处理调度、栈管理和线程复用。但 goroutine 之间需要通过 channel 或 sync 包 同步,否则会出现数据竞争。 ...

2026 年 10 月 13 日

select 多路复用

一个 goroutine 同时监听多个 channel 的「事件循环」:C++/epoll 监听多个 fd 的那个心智模型,Go 把它内建成了 select 语句 一、简述 select 是 Go 语法层面的多路复用(multiplexing)原语:它让一个 goroutine 同时等待多个 channel 上的收发操作,只要其中一个就绪,就走对应的分支。 对于 C++ 开发者,最贴切的类比是 Linux epoll / poll:你不想为每个 socket 各开一个线程去阻塞读,而是用一个线程同时监听一堆 fd,哪个有事件处理哪个。select 就是「 goroutine 版本的 epoll」——但它更进一步:它还能同时做非阻塞尝试(default)、定时超时(time.After)、动态禁用分支(nil channel)。 核心要点:select 是「组合 channel 为复杂同步逻辑」的胶水。没有 select,一个 goroutine 只能死等一个 channel;有了 select,你才能写超时、写取消、写事件循环、写多路汇流。 二、C++ 对照速查表 概念 C++ Go 关键差异 多路监听 epoll_wait(fds, ...) / poll() select { case <-ch1: ... } Go 是对 channel 通道复用,C++ 是对 fd 复用 同时等两个事件 一个线程一个 fd 各睡一觉 / epoll 一个 goroutine 同时等多个 case select 内建,代码直白得多 随机公平选择 epoll 按就绪顺序返回 多 case 就绪时随机选一个 Go 保证公平:随机而非先后 非阻塞尝试 poll(fd, timeout=0) select { case ...: default: } default = 都不就绪立即走 超时等待 epoll_wait 的 timeout 参数 select + time.After(1s) 超时是普通 channel,不是特殊机制 收发都监听 读 / 写事件分别注册 一个 select 里既有发送又有接收 case 语法上收发对称 监听无限器 服务端 accept 线程循环 for { select { ... } } 事件循环惯用法 特定事件暂时屏蔽 取消注册 fd case 的 channel 置 nil → 永久阻塞 nil channel 妙用 三、逐主题详解 3.1 select 基本语法 package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) // 两个 goroutine 各自往不同 channel 发 go func() { time.Sleep(100 * time.Millisecond); ch1 <- "来自 ch1" }() go func() { time.Sleep(300 * time.Millisecond); ch2 <- "来自 ch2" }() // select 会阻塞,直到某个 case 就绪 select { case msg := <-ch1: fmt.Println("收到:", msg) // 大概率先到 ch1(100ms 更快) case msg := <-ch2: fmt.Println("收到:", msg) } } 要点: ...

2026 年 10 月 13 日

sync 包(WaitGroup / Mutex / Once)

C++ 的 std::thread::join、std::mutex、std::once_flag,Go 用 sync 包给了一套轻量且更安全的等价物 一、简述 sync 包是 Go 标准库的同步原语仓库,核心成员: 类型 用途 C++ 对应 sync.WaitGroup 等一批 goroutine 全部结束 std::thread::join(一次等多个) sync.Mutex 互斥锁 std::mutex sync.RWMutex 读写锁 std::shared_mutex sync.Once 只执行一次(懒加载 / 单例) std::once_flag + std::call_once sync.Map 并发安全的 map(特化场景) 手写 lock + std::map / 无直接对应 sync.Cond 条件变量 std::condition_variable 核心要点:Go 的哲学是「尽量用 channel」,但同步原语(锁 / 分组等待 / 只跑一次)仍然有明确位置。与 C++ 的锁相比,Go 的锁不可拷贝、有 defer 配合的极简用法、内置检测工具,心智负担小得多。本笔记重点讲 WaitGroup、Mutex / RWMutex、Once,并简述 errgroup。 二、C++ 对照速查表 概念 C++ Go 关键差异 等待线程结束 t.join()(单个) wg := sync.WaitGroup{}; wg.Add(1); defer wg.Done(); wg.Wait() WaitGroup 一次等一批,Add 要提前 互斥锁 std::mutex m; m.lock(); m.unlock(); var mu sync.Mutex; mu.Lock(); defer mu.Unlock() Go 惯例用 defer 解锁,防漏 锁唯一性 lock 传递没意义 结构体字段还是值?拷贝锁 = 复制已锁状态 → 编译常见错 Go 锁内含 noCopy,拷贝会被提示 只初始化一次 std::once_flag + std::call_once var once sync.Once; once.Do(func(){...}) Go 写进一个 Do,天然幂等 读写锁 std::shared_mutex(C++17) sync.RWMutex + RLock()/RUnlock() 读读并发、写写互斥、写独占 可重入 std::recursive_mutex 才行 Mutex 不可重入,同 goroutine 再锁 = 自锁死 Go 明确拒绝重入,倒逼更好的设计 条件变量 std::condition_variable + predicate 循环 sync.Cond(少见)或直接 channel Go 社区倾向 channel 代替 cond 分组错误传播 手写 std::future 收集异常 errgroup.Group(golang.org/x/sync) Go 有现成库,等全部且带首个错误 三、逐主题详解 3.1 sync.WaitGroup:等一批 goroutine package main import ( "fmt" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 结束前把计数器减 1(defer 保证即使中途 panic 也会减,避免死锁) fmt.Printf("worker %d 开始\n", id) time.Sleep(100 * time.Millisecond) // 模拟干活 fmt.Printf("worker %d 结束\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) // ⚠️ Add 必须在 go 之前!计数器先 +1 go worker(i, &wg) // 注意传指针:WaitGroup 内部要改计数 } wg.Wait() // 阻塞直到所有 Done,相当于「等这一批全部 join」 fmt.Println("所有 worker 都结束了") } WaitGroup 规则: ...

2026 年 10 月 13 日

并发模式(worker pool / 扇入扇出 / pipeline)

C++ 里要自己造轮子的线程池、任务队列、流水线,Go 用 channel 组合出几种固定范式,一次学会受用终生 一、简述 「并发模式」不是语法,而是被反复验证有效的编排范式。Go 里它们几乎全由 channel + goroutine 组合而成:worker pool(工人池)、fan-out(扇出)、fan-in(扇入 / 汇聚)、pipeline(流水线)、限速器(rate limit)、错误传播。 对于 C++ 开发者,这些模式你多半见过、也手动实现过: 模式 C++ 里怎么做的 痛点 线程池 std::thread 数组 + std::queue 任务 + mutex 守卫 线程生命周期、任务派发、锁竞争都要自己管 扇出 把一个大任务拆 N 份发给 N 线程 怎么「等所有分片」+ 汇总 流水线 每个 stage 一个线程 + 队列 + condvar 关停时机难管 限速 令牌桶 / 手写信号量 又要造一个计数器 + 条件变量 Go 里这些平均不到 30 行,且语义全部显式可见。本笔记不讲抽象,直接给 6 个能 go run 的模板。 核心要点:所有模式都建立在「channel 即队列 + 同步」之上。模式 = 固定形状的 channel 拓扑 + 一组约定(谁 close、谁 range、谁 Wait)。 ...

2026 年 10 月 13 日

数据竞争与 race 检测

C++ 的 TSan / helgrind 你在用,Go 内置了一个更好使的:go run -race 一键找数据竞争 一、简述 数据竞争(data race):两个或多个 goroutine 并发访问同一内存位置,且至少一个是写操作,且没有任何同步(锁 / channel / 原子操作)约束它们之间的顺序。数据竞争的结果是未定义行为(UB):读到撕裂值、编译器优化后行为诡异、crash、或者——最恼人的——偶尔才出错。 C++ 里你熟知的工具:ThreadSanitizer(TSan)、-fsanitize=thread、helgrind。Go 的对应物是内置的 race detector: go run -race main.go # 直接跑并检测 go test -race ./... # 测试时检测(推荐) go build -race main.go # 构建带检测的二进制(性能有损,仅调试) 核心要点:Go race detector 不是「预防工具」,而是「逮现行犯」的探针——它监测每个内存访问,报告「两个 goroutine 同时访问同一地址」的现场。加了 -race,不等于程序没问题;没加 -race,问题照样存在。 修复手段三件套:Mutex、atomic、channel 传值。 二、C++ 对照速查表 概念 C++ Go 关键差异 数据竞争定义 两个线程无同步访问同一地址 同,goroutine 版 语义一致:至少一写 + 无同步 检测工具 TSan / -fsanitize=thread / helgrind go run -race / go test -race Go 内置,零配置,一行参数 检测原理 影子内存 + happens-before 分析 同样基于 happens-before 的 shadow 内存 实现同源(Go 4 个字节内存配 8 字节影子) 共享计数器 std::atomic<int> / mutex atomic.AddInt64(首选) Go 原子 / 锁 / channel 三种解法都有 map 并发写 std::unordered_map + mutex map 并发写 = 直接 panic Go 有额外保护:写中读/写检测 修复手段 锁 / 原子 / 无共享 Mutex / atomic / channel 传值 第三种(传值)是 Go 特色 内存模型文档 cppreference memory_order The Go Memory Model Go 文档短得多,先读「happen-before」 检测开销 慢 5~15 倍 运行慢 510 倍,内存多 510 倍 都只该在测试/CI 用 三、逐主题详解 3.1 什么是数据竞争 判定为数据竞争需要三条同时成立: ...

2026 年 10 月 13 日