Go语言并发编程临界区的使用

作者:

文章目录
  • 临界区是指: 访问共享资源的代码片段 需要同步机制保护的部分 同一时间只允许一个执行线程/goroutine进入的区域 关键的特性: 共享资源访问:涉及对共享内存、文件、设备等资源的读写操作 原子性需求:临界区内的操作应作为一个不可分割的单元执行 互斥/排他访问:必须确保同一时间只有一个执行流能进入临界区 有限停留:线程应尽快离开临界区,减少阻塞其他线程的时间
  • 概念 描述 临界区 需要保护的代码段(概念) 锁 保护临界区的实现机制(工具) 关系 锁用来划定和保护临界区,临界区是需要锁保护的代码范围
  • 1.最小化原则: 尽量减小临界区的范围 只包含必须同步的操作 // 不好:包含非必要操作 mu.Lock() data := fetchFromDatabase() // 耗时IO操作 sharedMap[key] = data mu.Unlock() // 更好:仅保护共享访问 data := fetchFromDatabase() mu.Lock() sharedMap[key] = data mu.Unlock() 2.简短执行 避免在临界区内执行耗时操作如:IO、复杂计算 典型临界区应能在微秒级完成 3.单一职责 一个临界去最好只保护一个共享资源 避免多个不相关资源共同用同一个锁 4.无嵌套原则 避免在临界区内调用可能获取其他锁的方法 防止死锁发生
  • 银行账户转账 type Account struct { mu sync.Mutex balance int } func (a *Account) Transfer(to *Account, amount int) error { // 按固定顺序加锁防止死锁 first, second := a, to if 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 -= amount to.balance += amount // 临界区结束 return nil } 深度解读: func (a *Account) Transfer(to *Account, amount int) error:定义了一个名为Transfer的方法,该方法属于Account类型的接收者,表示从一个账户向另一个账户转账。方法接收两个参数,to是指向目标账户的指针,amount是要转账的金额。返回值是error类型,用于处理可能出现的错误情况。 防止死锁的机制:在进行转账操作之前,首先对两个账户进行排序(通过比较两个账户指针的内存地址),确保总是先锁定地址较小的那个账户的互斥锁,然后再锁定地址较大的那个账户的互斥锁。这种按固定顺序加锁的策略可以有效避免两个或多个 goroutine 同时尝试锁定不同账户的互斥锁时出现的死锁情况。 互斥锁的使用:通过first.mu.Lock()和second.mu.Lock()分别锁定两个账户的互斥锁,确保在同一时间只有一个 goroutine 可以访问这两个账户的余额。defer first.mu.Unlock()和defer second.mu.Unlock()语句用于确保在函数执行完毕后,无论是否发生错误,最终都能释放这两个账户的互斥锁。 临界区:在两个账户的互斥锁都被成功锁定之后,就开始执行转账操作的临界区代码。首先检查转出账户的余额是否足够覆盖转账金额,如果余额不足,则返回一个错误信息"insufficient balance"。否则,从转出账户扣除相应的金额,并将该金额加到转入账户的余额中 到此这篇关于Go语言并发编程临界区的使用的文章就介绍到这了,更多相关Go语言 临界区内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客! 您可能感兴趣的文章: GO语言临界资源安全问题的深入理解
  • 目录
    • 基本定义
    • 在Go中临界区示例
      • 无保护的临界区(危险)
      • 使用Mutex保护的临界区
    • 临界区与锁的关系
      • 临界区的设计规范
        • 临界区保护机制对比
          • 1. 互斥锁(Mutex)
          • 2. 读写锁(RWMutex)
          • 3. 通道(Channel)
        • 常会遇到的问题
          • 1. 数据竞争(Data Race)
          • 2. 死锁(Deadlock)
          • 3. 活锁(Livelock)
        • 项目案例改编

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

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

          临界区是指:

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

          关键的特性:

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

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

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

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

          1.最小化原则:

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

          2.简短执行

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

          3.单一职责

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

          4.无嵌套原则

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

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

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

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

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

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

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

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

          银行账户转账

          type Account struct {
              mu      sync.Mutex
              balance int
          }
          
          func (a *Account) Transfer(to *Account, amount int) error {
              // 按固定顺序加锁防止死锁
              first, second := a, to
              if 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 -= amount
              to.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"。否则,从转出账户扣除相应的金额,并将该金额加到转入账户的余额中

          到此这篇关于Go语言并发编程临界区的使用的文章就介绍到这了,更多相关Go语言 临界区内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!

          您可能感兴趣的文章:

          • GO语言临界资源安全问题的深入理解

          站内搜索