《Go程序设计语言》- 第9章:使用共享变量实现并发

前言

本专栏是笔者在学习《Go程序设计语言》这本书时,对每个章节认为较为重要容易忘记👻)的知识点记录的笔记,其中也会有少量的思考👀, 现整理成博客分享出来。

如果对专栏感兴趣,跑过去看一眼,书中的每一章都有:《Go程序设计语言》笔记

❗️注意❗️:本专栏不是详细的知识讲解,只是碎片的知识条目,或可作为Go知识点查漏补缺的小工具~

竞态

  1. 如果无法肯定地说一个事件必定先于另一个事件,那这两个事件就是并发的;

  2. 一个能在串行程序中正确工作的函数,如果并发调用时仍能正常工作,则该函数是**并发安全(concurrency-safe)**的;

  3. 要回避并发访问,要么变量只存在在一个goroutine内,要么维护一个更高层的互斥不变量

  4. 竞态是指多个goroutine按某些交错顺序进行时,无法给出正确的结果;竞态很难再现和分析;

  5. 数据竞态发生于两个goroutine并发读取某个数据,且至少一个写入数据;

  6. 避免数据竞态的三种办法:

    • 数据初始化后不再写入;
    • 避免从多个goroutine中访问同一个变量;
    • 互斥机制:同一时间只允许一个goroutine访问变量;
  7. 监控goroutine:使用通道请求来代理一个受限变量的所有访问,这个goroutine就是可以称为变量的goroutine

    • 比如存取余额时:

    • package bank
      // 将存钱和取钱的数值,都通过通道传递
      var deposits = make(chan int)
      var balances = make(chan int)
      func Deposit(amount int){ deposits<-amount}
      func Balance()int{return balances }
      
      func teller(){
          // 在goroutine中处理通道传递的数值,实现代理
          var balance int
          for{
              // select保证同一时刻只发生一个
              select{
                  case amount := <-deposits:
                  	balance += amount
                  case balances <- balance:
              }
          }
      }
      func init(){
          // 初始化时打开balance变量的监控goroutine
          go teller()
      }
      
  8. 不要通过共享内存来通信,而是通过通信来共享内存

互斥锁:sync.Mutex

  1. 可以使用一个容量为1的通道来保证同一时间最多有1goroutine能访问共享变量;sync.Mutex实现了该机制;
    • 使用mu.Lock()获取令牌;
    • 使用mu.Unlock()释放令牌;
    • Lock()Unlock()之间的部分被称为临界区
    • 当函数结构比较复杂时,可以使用defer mu.Unlock()来确保释放锁,但执行成本会增大一些,后续可以优化;

读写互斥锁:sync.RWMutex

  1. 多读单写锁:只读操作可以并发执行,但写操作需要独享变量;
  2. sync.RWMutex实现了多读单写锁,使用mu.RLock()、mu.RUnlock()来获取、释放读锁;
  3. 多读单写锁内部实现更复杂,所以仅在竞争较为激烈(每次都需要等待才能获取锁)时才使用;竞争不激烈时使用sync.Mutex即可,效率更高;

内存同步

  1. 并不只是自己操作会被打断时才加锁,也有可能是为避免打断他人操作才加锁;

  2. 同步不仅涉及多个goroutine的执行顺序,还会影响到内存

  3. 考虑下面的程序,有可能输出是:x=0 y=0,原因是有可能内存还没被同步

    var x int
    var y int
    go func(){
        x = 1
        fmt.Print("y=", y)
    }
    go func(){
        y = 1
        fmt.Print("x=", x)
    }
    
  4. 并发问题最好从机制上避免:尽可能将变量限制在一个goroutine内;其它情况使用互斥锁;

延迟初始化sync.Once

  1. 预先初始化一个变量会增加程序的启动延时

  2. sync.Once概念上包含一个bool和一个互斥量,bool检测是否初始化成功,互斥量保护这个bool和客户端的数据结构;它包含唯一方法sync.Once.Do(),传入一个初始化函数;

    • 实际实现:sync.Once

    • type Once struct{
          done uint32
          m Mutex
      }
      func (o *Once) Do(f func()){...}
      func (o *Once) doSlow(){...}
      
  3. sync.Once可以在只需要初始化一次的场合,使用简单的逻辑来避免重复初始化;

竞态检测器

  1. go rungo buildgo test命令中使用-race即可使用go的竞态检测器(race detector);
  2. 竞态检测器会研究事件流,找到那些有问题的案例,报告出检测到的数据竞态;
  3. 它只能检测运行时发生了的竞态,而不能保证不会发生竞态;

示例:并发非阻塞缓存

goroutine与线程

  1. 一个goroutine在生命周期开始时只有一个很小的栈,典型情况下为2KB;用来存放那些正在执行或临时暂停的函数中的局部变量;
  2. 栈可增长至1GB
  3. OS线程由内核每隔几毫秒调度;go运行时还有一个自己的调度器:采用m:n调度技术,可以复用mgoroutinenOS线程;
  4. go调度器不由硬件时钟触发,而是由语言结构(如time.Sleep)触发调度;所以goroutine的调度比线程调度的成本低很多;
  5. go调度器使用GOMAXPROCS参数来确定使用多少个OS线程来同时执行go代码;默认为CPU数量;
  6. goroutine没有标识,线程一般是有标识的;

如有错误 ❌ ,欢迎指正 ☝️~

如有收获 🍗,可以考虑点赞👍/评论💬/收藏⭐️/关注👀,大家共同进步~


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值