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

 更新时间:2025年09月26日 09:13:58   作者:Jayden_念旧  
本文主要介绍了Go语言并发编程临界区的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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

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

基本定义

临界区是指:

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

关键的特性:

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

在Go中临界区示例

无保护的临界区(危险)

var counter int // 共享变量

func increment() {
    counter++ // 这就是临界区(没有保护)
}

使用Mutex保护的临界区

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.无嵌套原则

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

临界区保护机制对比

1. 互斥锁(Mutex)

var mu sync.Mutex

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

2. 读写锁(RWMutex)

var rwMu sync.RWMutex

func 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.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设计模式之备忘录模式讲解和代码示例

    Go设计模式之备忘录模式讲解和代码示例

    备忘录是一种行为设计模式, 允许生成对象状态的快照并在以后将其还原,本文就通过代码示例给大家讲讲Go备忘录模式,感兴趣的小伙伴跟着小编一起来看看吧
    2023-08-08
  • Go语言中的数据格式(json、xml 、msgpack、protobuf)使用总结

    Go语言中的数据格式(json、xml 、msgpack、protobuf)使用总结

    在分布式的系统中,因为涉及到数据的传输,所以一定会进行数据的交换,此时就要定义数据交换的格式,例如二进制、Json、Xml等等。本文总结了Go语言中的数据格式,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • 如何使用Go语言实现远程执行命令

    如何使用Go语言实现远程执行命令

    远程执行命令最常用的方法就是利用SSH协议,将命令发送到远程机器上执行,并获取返回结果。本文将介绍如何使用Go语言实现远程执行命令。下面一起来看看。
    2016-08-08
  • Go语言中的复合类型详细介绍

    Go语言中的复合类型详细介绍

    这篇文章主要介绍了Go语言中的复合类型详细介绍,复合类型包括:结构体、数组、切片、Maps,需要的朋友可以参考下
    2014-10-10
  • 在golang xorm中使用postgresql的json,array类型的操作

    在golang xorm中使用postgresql的json,array类型的操作

    这篇文章主要介绍了在golang xorm中使用postgresql的json,array类型的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Go Web下gin框架的模板渲染的实现

    Go Web下gin框架的模板渲染的实现

    Gin框架是目前非常流行的Go语言Web框架之一,作为一个轻量级的框架,Gin提供了丰富的功能和灵活的架构,本文就来介绍下Go Web下gin框架的模板渲染的实现,感兴趣的可以了解一下
    2023-10-10
  • 浅谈beego默认处理静态文件性能低下的问题

    浅谈beego默认处理静态文件性能低下的问题

    下面小编就为大家带来一篇浅谈beego默认处理静态文件性能低下的问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Golang判断两个链表是否相交的方法详解

    Golang判断两个链表是否相交的方法详解

    这篇文章主要为大家详细介绍了如何通过Golang判断两个链表是否相交,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-03-03
  • Golang中switch语句和select语句的用法教程

    Golang中switch语句和select语句的用法教程

    这篇文章主要给大家介绍了关于Golang中switch和select的用法教程,文中通过示例代码将switch语句与select语句的使用方法介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面跟着小编一起来学习学习吧。
    2017-06-06
  • Go/C语言LeetCode题解997找到小镇法官

    Go/C语言LeetCode题解997找到小镇法官

    这篇文章主要为大家介绍了Go语言LeetCode题解997找到小镇的法官示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12

最新评论