zoukankan      html  css  js  c++  java
  • 在Golang中是锁或Channel还是Atomic

      与其他编程语言一样在并发环境下如不对多个goroutine(线程)访问或修改的共享资源元素的进行控制,让进入临界区的对象互斥。就可能会出现数据异常情况;
      一个非线程安全对象如下,如不对Id的访问进行控制,多个goroutine进行更新Id字段是就会出现数据不一致的情况,如下示例:

    type Conf struct {
      Id int32
    }
    func(c *Conf)update(n int32){
      c.Id +=n
    }
    

      启动100个goroutine用于更新对象c中的Id字段值,此时由于出现多个协程同时进入临界区同时对Id变量进行修改。导致对象c中的Id字段值出现了不可预知的情况。此时程序输出的结果可能是:98、93、95等;

    func main() {
     var c=&Conf{}
     for i := 0; i<100;i++  {
    	go func(n int) {
    		//模拟停顿
    		time.Sleep(1*time.Millisecond)
    		c.update(1)
    	}(i)
     }
    time.Sleep(10*time.Second)
    fmt.Println(c.Id)
    }
    

      下面分别使用锁与channel对临界区进行并发控制,使得输出得到正常的结果,并简单对比两者的性能;

    使用锁

      现在在结构体Conf中添加一个读写锁变量sync.RWMutex,此时Conf继承了RWMutex中所有的方法字段等。通过此对象就可以对Id变量的访问进行加锁, struct变为如下:

    type Conf struct {
      Id int32
      sync.RWMutex
    }
    

    定义一个新方法,此方法使用了锁对临界区访问进行了并发控制:

    func(c *Conf) updateOfLock(n int32){
       c.Lock()
       defer c.Unlock()
       c.Id +=n
    }
    

      此时程序总是能够输出正确的结果:100

    func main() {
    var c=&Conf{}
    for i := 0; i<100;i++  {
    	go func(n int) {
    		//模拟停顿
    		time.Sleep(1*time.Millisecond)
    		c.updateOfLock(1)
    	}(i)
    }
    time.Sleep(10*time.Second)
    fmt.Println(c.Id)
    }
    

    使用channel

      用channel控制临界资源的访问,原理也非常简单,创建一个长度为1的channel变量,进去临界区前往channel中写入一个值,由于长度为1,此时别的协程将无法往channel中写入值堵塞在此处channel上,前一个协程访问完临界区后从channel读取值,此后别的协程可再此往channel中写入值,从而能够进入临界区,结构体修改如下;

    type Conf struct {
       Id int32
       lockChan chan bool
    }
    func (c *Conf) updateOfChannel(n int32) {
       c.lockChan<-true
       defer func() {<-c.lockChan}()
       c.Id +=n
    }
    

      使用channel进行并发控制,需要注意取出lockChan中的值,此处使用了defer用于控制channel值的释放。

     func main() {
        var c=&Conf{lockChan: make(chan bool,1)}
         for i := 0; i<100;i++  {
    	go func(n int) {
    		//模拟停顿
    		time.Sleep(1*time.Millisecond)
    		c.updateOfChannel(1)
    	}(i)
         }
    time.Sleep(10*time.Second)
    fmt.Println(c.Id)
    }
    

    原子变量

      无需修改Conf结构体,直接调用atomic的相关方法即可,方法如下,此时也是能够得到正确的值;

      func (c *Conf) updateOfAtomic(n int32) {
        atomic.AddInt32(&c.Id,n)
      }
    

    性能简单对比

      通过跑Golang基准测试看看锁、原子变量、channel三者的性能如何,毫无疑问原子变量肯定是性能最好的,这是对底层的CAS操作连临界区都没有产生;这里主要对比锁与channel性能,三个测试函数都和下函数类似;

     func BenchmarkAtomicTest(b *testing.B) {
        var c=&Conf{}
         for i := 0; i<b.N;i++  {
           c.updateOfAtomic(1)
        }
    }
    

      此时可以看到毫无悬念的atomic原子操作的方式性能最高,比其他两种低一个数量级,锁的方式比channel时间快了一倍,每次平均执行时间只需44.45纳秒,而channel每次需要84.12ns纳秒;当然此处的只是简单对比;

      能用atomic时肯定是用atomic,至于锁和channel选哪个,还是要看应用场景、业务逻辑、可维护性等,总体来说两者性能差别不算太大,如果不太熟悉channel在过于复杂的业务逻辑中channel或许可读性会降低;但golang中的两个核心就是goroutine和channel;

  • 相关阅读:
    inux按照CPU、内存、磁盘IO、网络性能监测
    监控数据库
    NMON监控工具
    AJAX
    性能经验之谈【转】
    内存/硬盘/io关系
    testng中添加案例失败重试次数
    如何使用beanshell写入数据到文件(txt、csv)
    Docker 容器中配置nginx后报403 Forbidden 解决办法
    Centos7创建支持ssh服务器的docker容器
  • 原文地址:https://www.cnblogs.com/softlin/p/14901638.html
Copyright © 2011-2022 走看看