当前位置:首页 > 文章列表 > Golang > Go教程 > Go 怎样用 atomic.Bool 实现一次开关

Go 怎样用 atomic.Bool 实现一次开关

来源:17golang原创 2026-10-17 17:32:21 0浏览 收藏

如果一个初始化动作、迁移动作或首次注册动作只能成功一次,可以把状态放进 atomic.Bool,让所有调用共享同一个开关,再用 CompareAndSwap(false, true) 抢占唯一的成功机会。返回 true 的调用继续执行后续动作,其他调用直接结束;这个判断和状态修改是一个原子操作,不会因为并发读写而产生两个“首次成功者”。

这里的“一次”指从 false 到 true 的一次条件转换。如果业务允许重新打开,就需要明确调用 Store(false) 的时机;如果状态还要和订单、锁定时间等字段保持整体一致,单独一个布尔原子值并不能替代事务或更大范围的同步。

先把一次开关的状态说清楚

atomic.Bool 是 sync/atomic 提供的类型化原子布尔值,零值为 false,常用方法包括 Load、Store、Swap 和 CompareAndSwap。它适合表示一个可以独立读取、独立改变的状态,例如“是否已经完成初始化”“是否已经发送首个通知”。

我在这种场景里通常先把对象身份固定下来:并发调用拿到的是同一个 *OnceSwitch,而不是每次函数调用新建一个局部变量。只有共享同一个原子字段,竞争才会发生在同一个状态上。

一次开关对象与 atomic.Bool 访问修改方法的静态关系
图1:一次开关对象、atomic.Bool 以及 Load、Store、Swap、CompareAndSwap 的静态 API 关系说明图,不是运行截图。

用 CompareAndSwap 让并发调用只成功一次

一次性动作最适合使用 CompareAndSwap:只有当当前值仍是 false 时,才把它替换成 true,并返回是否替换成功。多个 goroutine 同时尝试时,只有一个调用能满足这个条件;其他调用看到旧值已经不是 false,就会返回 false。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

// OnceSwitch 把一次性状态集中到一个不可复制的共享对象中。
type OnceSwitch struct {
	started atomic.Bool
}

// TryStart 只让第一个调用者取得后续动作的执行权。
func (s *OnceSwitch) TryStart() bool {
	// 比较和写入在同一个原子操作内完成,避免两个调用都成功。
	return s.started.CompareAndSwap(false, true)
}

func main() {
	switcher := new(OnceSwitch) // 使用同一个指针,所有调用竞争同一状态。
	var wg sync.WaitGroup
	const callers = 4

	wg.Add(callers)
	for i := 0; i 

这个示例的关键不是 goroutine 数量,而是 switcher 只创建一次,并把指针交给所有调用者。预期只有一行 winner,但具体是哪一个编号并不固定。这里的输出是根据代码逻辑说明的预期,不是本文制作时执行得到的运行截图。

如果一次性动作本身可能失败,要先决定状态表示的是“已经抢到执行权”还是“动作已经成功完成”。上面的写法表示前者:一旦某个调用把状态改成 true,后续调用就不会再进入动作。若业务要求失败后允许重试,就不能在失败时无条件保持这个状态,应重新设计回滚或状态机边界。

Load、Store 和 Swap 分别解决什么问题

Load 只读取当前布尔值,适合做展示、采样或在不需要抢占时判断状态。它不会改变开关,也不能把“先读取再执行”自动变成一次性竞争。

// IsStarted 返回一次开关在某个读取时刻的快照。
func (s *OnceSwitch) IsStarted() bool {
	return s.started.Load() // 原子读取,不承担后续动作的排他性。
}

Store 是无条件写入。初始化时可以用它明确设置状态,测试或明确的生命周期切换也可以用它重置;但 Store(true) 不会告诉你之前是谁改的,Store(false) 也会重新打开一次性入口。

// Reset 只有在业务确认允许重新开始时才调用。
func (s *OnceSwitch) Reset() {
	s.started.Store(false) // 重置后下一次 CompareAndSwap 可能再次成功。
}

Swap 会原子地写入新值并返回旧值。如果你需要知道“切换前是否已经打开”,它比单独 Load 再 Store 更合适,因为读取旧值和写入新值不会被另一个调用插入。

// OpenAndReport 打开开关,并报告这次写入前的旧状态。
func (s *OnceSwitch) OpenAndReport() (wasOpen bool) {
	return s.started.Swap(true) // 返回旧值,false 表示此前处于关闭状态。
}

不要把 Load 加 Store 当成一次开关

下面这种写法看起来直观,但在并发下不成立:

// wrongTryStart 把判断和写入拆开,不能保证只有一个成功者。
func (s *OnceSwitch) wrongTryStart() bool {
	if s.started.Load() { // 两个调用都可能同时读到 false。
		return false
	}
	s.started.Store(true) // 另一个调用可能已经在这里之前完成了同样的写入。
	return true
}

每一次 Load 和 Store 本身都可以是原子的,但两次调用之间的业务判断不是原子的。两个调用都读到 false 后,就可能都返回 true。要竞争“旧值是 false 的那个机会”,必须直接使用 CompareAndSwap(false, true)。

共享 atomic.Bool、唯一成功者与业务一致性边界的静态结构
图2:共享原子布尔值、唯一成功者、重复调用与业务后续动作的静态边界说明图,不是运行结果或执行流程。

对象不能复制,业务动作也不能只靠一个字段

原子类型使用后不要复制。不要把包含 atomic.Bool 的结构体按值传递、按值返回或用赋值制造第二份对象;需要共享时传递指针,需要对外提供结果时读取普通的 bool 快照。复制对象会让调用者误以为自己仍在竞争同一个开关。

还要把“状态更新”和“业务动作完成”分开。CompareAndSwap 只能保证布尔字段的这次条件替换;它不会自动把数据库写入、文件创建、日志记录等多个动作包成事务。如果动作需要和其他字段保持一致,就用事务、互斥锁或明确的状态机覆盖整个一致性范围。

最后,原子性也不等于执行顺序已经满足业务要求。成功取得一次开关后,调用者仍要负责错误处理、资源清理和幂等设计;如果动作失败后必须重试,就不要把“抢到执行权”直接当成“业务已完成”。

怎么选择这四个方法

方法适合的任务要注意的边界
Load读取当前快照不能和后续动作组成一次性竞争
Store无条件设置或明确重置会覆盖当前状态,重置会重新开放入口
Swap写入新值并拿到旧值表示替换,不表达复杂业务条件
CompareAndSwap只有旧值匹配时才更新最适合争抢唯一的首次执行权

实际使用时可以按一个问题判断:你需要的是“看一眼状态”、“直接覆盖状态”、“知道替换前是什么”,还是“只有状态仍为某值才允许我改写”?一次开关通常属于最后一种,因此从 CompareAndSwap(false, true) 开始设计最清楚。

官方文档入口

官方地址:https://pkg.go.dev/sync/atomic

版本说明:https://go.dev/doc/go1.19

内存模型:https://go.dev/ref/mem

相关问题

  • atomic.Bool 和互斥锁怎么选?只保护一个独立布尔状态时原子类型更直接;需要同时保护多个字段或一段临界区时,互斥锁通常更容易表达整体不变量。

  • 为什么不能先 Load 再 Store?因为判断和写入之间存在竞争窗口,多个调用都可能基于旧的 false 返回成功。

  • 一次性动作失败后能不能重试?可以,但要把“已抢到执行权”和“动作已完成”拆开,不能无条件把失败当作成功状态。

  • Store(false) 会发生什么?它会重新打开开关,之后另一个调用可能再次通过 CompareAndSwap(false, true)。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go 怎样复制 map 并避免共享底层数据Go 怎样复制 map 并避免共享底层数据
上一篇
Go 怎样复制 map 并避免共享底层数据
Go 怎样按自定义规则比较两个 map
下一篇
Go 怎样按自定义规则比较两个 map
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    505次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    564次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    590次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    543次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    356次使用