当前位置: 首页 > news >正文

Go语言并发编程 ------ 临界区

临界区是多线程/并发编程中的核心概念,指程序中访问共享资源(如变量、数据结构、文件等)的代码段,这些资源在同一时间只能被一个线程访问以避免数据竞争和不一致。

本篇文章着重介绍临界区,锁的详细介绍会在下一篇文章中。

基本定义

临界区是指:

  • 访问共享资源的代码片段
  • 需要同步机制保护的部分
  • 同一时间只允许一个执行线程/goroutine进入的区域

关键的特性:

  1. 共享资源访问:涉及对共享内存、文件、设备等资源的读写操作
  2. 原子性需求:临界区内的操作应作为一个不可分割的单元执行
  3. 互斥/排他访问:必须确保同一时间只有一个执行流能进入临界区
  4. 有限停留:线程应尽快离开临界区,减少阻塞其他线程的时间

在Go中临界区示例

无保护的临界区(危险)

var counter int // 共享变量func increment() {counter++ // 这就是临界区(没有保护)
}

使用Mutex保护的临界区

var (counter intmu      sync.Mutex
)func safeIncrement() {mu.Lock()         // 进入临界区前加锁defer mu.Unlock() // 确保退出时解锁counter++         // 受保护的临界区// 其他操作...
}

临界区与锁的关系

概念描述
临界区需要保护的代码段(概念)
保护临界区的实现机制(工具)
关系锁用来划定和保护临界区,临界区是需要锁保护的代码范围

临界区的设计规范

1.最小化原则:

  • 尽量减小临界区的范围
  • 只包含必须同步的操作
   // 不好:包含非必要操作mu.Lock()data := fetchFromDatabase() // 耗时IO操作sharedMap[key] = datamu.Unlock()// 更好:仅保护共享访问data := fetchFromDatabase()mu.Lock()sharedMap[key] = datamu.Unlock()

2.简短执行

  • 避免在临界区内执行耗时操作如:IO、复杂计算
  • 典型临界区应能在微秒级完成

3.单一职责

  • 一个临界去最好只保护一个共享资源
  • 避免多个不相关资源共同用同一个锁

4.无嵌套原则

  • 避免在临界区内调用可能获取其他锁的方法
  • 防止死锁发生

临界区保护机制对比

1. 互斥锁(Mutex)

var mu sync.Mutexfunc accessShared() {mu.Lock()// 临界区...mu.Unlock()
}

2. 读写锁(RWMutex)

var rwMu sync.RWMutexfunc readShared() {rwMu.RLock()// 只读临界区(允许多个读者)rwMu.RUnlock()
}func writeShared() {rwMu.Lock()// 写临界区(独占)rwMu.Unlock()
}

3. 通道(Channel)

var ch = make(chan struct{}, 1) // 容量1的通道模拟锁func accessShared() {ch <- struct{}{} // 获取"锁"// 临界区...<-ch // 释放"锁"
}

常会遇到的问题                                                            

1. 数据竞争(Data Race)

// 两个goroutine并发执行此函数会导致数据竞争
func race() {counter++ // 未保护的临界区
}

检测:使用go run -racego test -race

2. 死锁(Deadlock)

func deadlock() {mu.Lock()mu.Lock() // 重复加锁导致死锁mu.Unlock()mu.Unlock()
}

3. 活锁(Livelock)

// 两个goroutine不断重试但无法进展
func livelock() {for {if mu.TryLock() { // Go 1.18+// 临界区...mu.Unlock()break}time.Sleep(time.Millisecond) // 可能导致活锁}
}

项目案例改编

银行账户转账

type Account struct {mu      sync.Mutexbalance int
}func (a *Account) Transfer(to *Account, amount int) error {// 按固定顺序加锁防止死锁first, second := a, toif a < to { // 通过地址比较确定顺序first, second = to, a}first.mu.Lock()defer first.mu.Unlock()second.mu.Lock()defer second.mu.Unlock()// 临界区开始if a.balance < amount {return errors.New("insufficient balance")}a.balance -= amountto.balance += amount// 临界区结束return nil
}

深度解读:

  1. func (a *Account) Transfer(to *Account, amount int) error:定义了一个名为Transfer的方法,该方法属于Account类型的接收者,表示从一个账户向另一个账户转账。方法接收两个参数,to是指向目标账户的指针,amount是要转账的金额。返回值是error类型,用于处理可能出现的错误情况。
  2. 防止死锁的机制:在进行转账操作之前,首先对两个账户进行排序(通过比较两个账户指针的内存地址),确保总是先锁定地址较小的那个账户的互斥锁,然后再锁定地址较大的那个账户的互斥锁。这种按固定顺序加锁的策略可以有效避免两个或多个 goroutine 同时尝试锁定不同账户的互斥锁时出现的死锁情况。
  3. 互斥锁的使用:通过first.mu.Lock()和second.mu.Lock()分别锁定两个账户的互斥锁,确保在同一时间只有一个 goroutine 可以访问这两个账户的余额。defer first.mu.Unlock()和defer second.mu.Unlock()语句用于确保在函数执行完毕后,无论是否发生错误,最终都能释放这两个账户的互斥锁。
  4. 临界区:在两个账户的互斥锁都被成功锁定之后,就开始执行转账操作的临界区代码。首先检查转出账户的余额是否足够覆盖转账金额,如果余额不足,则返回一个错误信息"insufficient balance"。否则,从转出账户扣除相应的金额,并将该金额加到转入账户的余额中
http://www.xdnf.cn/news/1315387.html

相关文章:

  • 批次号规则
  • Mac(四)自定义按键工具 Hammerspoon 的安装和使用
  • FX10/20 (CYUSB401X)开发笔记5 固件架构
  • 基于DSP+ARM+FPGA架构的储能协调控制器解决方案,支持全国产化
  • 【完整源码+数据集+部署教程】无人机航拍视角洪水检测与受灾房屋识别图像分割救援指导系统源码和数据集:改进yolo11-DCNV2
  • Tomcat下载、安装及配置详细教程
  • STL 容器
  • Kotlin集合概述
  • 第16节:自定义几何体 - 从顶点构建3D世界
  • 【MySQL学习|黑马笔记|Day7】触发器和锁(全局锁、表级锁、行级锁、)
  • 《Python学习之文件操作:从入门到精通》
  • Linux 服务:iSCSI 存储服务配置全流程指南
  • Java基础面试题(3)—Java(String字符串的存储方式,字面量)
  • 链表OJ题讲解---试金石含金量
  • 6个日常工作中常用的工作法:清单工作法、PDCA循环、SMART原则、6W2H 分析法等方法
  • CSS中linear-gradient 的用法
  • 《Vuejs设计与实现》第 14 章(内建组件和模块)
  • Docker+飞算JavaAI=未来:全流程容器化AI开发实战
  • Matlab课程实践——基于MATLAB设计的计算器软件(简单、科学、电工、矩阵及贷款计算)
  • python实现梅尔频率倒谱系数(MFCC) 除了傅里叶变换和离散余弦变换
  • p5.js 3D 形状 “预制工厂“——buildGeometry ()
  • Mitt 事件发射器完全指南:200字节的轻量级解决方案
  • fastadmin 后台列表自定义搜索
  • 【递归、搜索与回溯算法】记忆化搜索
  • 当 AI 开始 “理解” 情感:情感计算技术正在改写人机交互规则
  • KingbaseES:一体化架构与多层防护,支撑业务的持续稳定运行与扩展
  • geekbench riscv镜像下载
  • 【Virtual Globe 渲染技术笔记】8 顶点变换精度
  • 提升 LLM 推理效率的秘密武器:LM Cache 架构与实践
  • Node.js导入MongoDB具体操作