golang中的原子 *** 作

golang中的原子 *** 作,第1张

简介 加锁代价比较耗时,需要上下文切换针对基本数据类型,可以使用原子 *** 作保证线程安全原子 *** 作在用户态就可以完成,因此性能比互斥锁要高

sync/atomic包中的函数可以做的原子 *** 作有:

加法(add)比较并交换(compare and swap,简称 CAS)加载(load)存储(store)交换(swap)

原子 *** 作函数需要的是被 *** 作值的指针,而不是这个值本身

只要原子 *** 作函数拿到了被 *** 作值的指针,就可以定位到存储该值的内存地址。只有这样,它们才能够通过底层的指令,准确地 *** 作这个内存地址上的数据。

支持的类型

这些函数针对的数据类型并不多。但是,对这些类型中的每一个,

sync/atomic包都会有一套函数给予支持。这些数据类型有:int32、int64、uint32、uint64、uintptr,以及unsafe包中的Pointer。

不过,针对unsafe.Pointer类型,该包并未提供进行原子加法 *** 作的函数。

减法

atomic.AddInt32函数的第二个参数代表差量,它的类型是int32,是有符号的。如果我们想做原子减法,那么把这个差量设置为负整数就可以了。

对于atomic.AddInt64函数来说也是类似的。不过,要想用atomic.AddUint32和atomic.AddUint64函数做原子减法,就不能这么直接了,因为它们的第二个参数的类型分别是uint32和uint64,都是无符号的,不过,这也是可以做到的,就是稍微麻烦一些。

例如,如果想对uint32类型的被 *** 作值18做原子减法,比如说差量是-3,那么我们可以先把这个差量转换为有符号的int32类型的值,然后再把该值的类型转换为uint32,用表达式来描述就是uint32(int32(-3))。

不过要注意,直接这样写会使 Go 语言的编译器报错,它会告诉你:“常量-3不在uint32类型可表示的范围内”,换句话说,这样做会让表达式的结果值溢出。

不过,如果我们先把int32(-3)的结果值赋给变量delta,再把delta的值转换为uint32类型的值,就可以绕过编译器的检查并得到正确的结果了。

最后,我们把这个结果作为atomic.AddUint32函数的第二个参数值,就可以达到对uint32类型的值做原子减法的目的了。

还有一种更加直接的方式。我们可以依据下面这个表达式来给定atomic.AddUint32函数的第二个参数值:

^uint32(-N-1))

其中的N代表由负整数表示的差量。也就是说,我们先要把差量的绝对值减去1,然后再把得到的这个无类型的整数常量,转换为uint32类型的值,最后,在这个值之上做按位异或 *** 作,就可以获得最终的参数值了。

这么做的原理也并不复杂。简单来说,此表达式的结果值的补码,与使用前一种方法得到的值的补码相同,所以这两种方式是等价的。我们都知道,整数在计算机中是以补码的形式存在的,所以在这里,结果值的补码相同就意味着表达式的等价

比较并交换和交换 *** 作相比有什么不同?

比较并交换 *** 作即 CAS *** 作,是有条件的交换 *** 作,只有在条件满足的情况下才会进行值的交换。

所谓的交换指的是,把新值赋给变量,并返回变量的旧值。

在进行 CAS *** 作的时候,函数会先判断被 *** 作变量的当前值,是否与我们预期的旧值相等。如果相等,它就把新值赋给该变量,并返回true以表明交换 *** 作已进行;否则就忽略交换 *** 作,并返回false。

可以看到,CAS *** 作并不是单一的 *** 作,而是一种 *** 作组合。这与其他的原子 *** 作都不同。正因为如此,它的用途要更广泛一些。例如,我们将它与for语句联用就可以实现一种简易的自旋锁(spinlock)。

for {
   if atomic.CompareAndSwapInt32(&num2, 10, 0) {
   fmt.Println("The second number has gone to zero.")
   break
}
   time.Sleep(time.Millisecond * 500)
}

for语句加 CAS *** 作的假设往往是:共享资源状态的改变并不频繁,或者,它的状态总会变成期望的那样。这是一种更加乐观,或者说更加宽松的做法。

方法

目前只支持int类型

import (
   "fmt"
   "sync"
   "sync/atomic"
)

var x int32
var wg sync.WaitGroup

func add() {
   for i := 0; i<5000; i++ {
      //x = x+1
      atomic.AddInt32(&x, 1)
   }
   defer wg.Done()
}

func main() {
   wg.Add(2)
   //各加5000
   go add()
   go add()
   wg.Wait()
   //结果一定是10000
   fmt.Println(x)
}
自旋锁
// forAndCAS1 用于展示简易的自旋锁。
func forAndCAS1() {
   sign := make(chan struct{}, 2)
   num := int32(0)
   fmt.Printf("The number: %d\n", num)
   go func() { // 定时增加num的值。
      defer func() {
         sign <- struct{}{}
      }()
      for {
         time.Sleep(time.Millisecond * 500)
         newNum := atomic.AddInt32(&num, 2)
         fmt.Printf("The number: %d\n", newNum)
         if newNum == 10 {
            break
         }
      }
   }()
   go func() { // 定时检查num的值,如果等于10就将其归零。
      defer func() {
         sign <- struct{}{}
      }()
      for {
         if atomic.CompareAndSwapInt32(&num, 10, 0) {
            fmt.Println("The number has gone to zero.")
            break
         }
         time.Sleep(time.Millisecond * 500)
      }
   }()
   <-sign
   <-sign
}
atomic.value

atomic.Value分为两个 *** 作,通过Store()存储Value,通过Load()来读取Value的值.

package main

import (
   "fmt"
   "sync/atomic"
)

type Value struct {
   key string
   Val interface{}
}

type Noaway struct {
   Movies atomic.Value
   Total atomic.Value
}

func NewNoaway() *Noaway {
   n := new(Noaway)
   n.Movies.Store(&Value{key: "movie", Val: "Wolf Warrior 2"})
   n.Total.Store("123")
   return n
}

func main() {
   n := NewNoaway()
   val := n.Movies.Load().(*Value)
   total := n.Total.Load().(string)
   fmt.Printf("%#v --- %#v\n", val, total)
}
如何用好atomic.value

第一条规则,不能用原子值存储nil。

也就是说,我们不能把nil作为参数值传入原子值的Store方法,否则就会引发一个 panic。

这里要注意,如果有一个接口类型的变量,它的动态值是nil,但动态类型却不是nil,那么它的值就不等于nil。我在前面讲接口的时候和你说明过这个问题。正因为如此,这样一个变量的值是可以被存入原子值的。

第二条规则,我们向原子值存储的第一个值,决定了它今后能且只能存储哪一个类型的值。

例如,我第一次向一个原子值存储了一个string类型的值,那我在后面就只能用该原子值来存储字符串了。如果我又想用它存储结构体,那么在调用它的Store方法的时候就会引发一个 panic。这个 panic 会告诉我,这次存储的值的类型与之前的不一致。

你可能会想:我先存储一个接口类型的值,然后再存储这个接口的某个实现类型的值,这样是不是可以呢?

很可惜,这样是不可以的,同样会引发一个 panic。因为原子值内部是依据被存储值的实际类型来做判断的。所以,即使是实现了同一个接口的不同类型,它们的值也不能被先后存储到同一个原子值中。

遗憾的是,我们无法通过某个方法获知一个原子值是否已经被真正使用,并且,也没有办法通过常规的途径得到一个原子值可以存储值的实际类型。这使得我们误用原子值的可能性大大增加,尤其是在多个地方使用同一个原子值的时候。

下面,我给你几条具体的使用建议。

不要把内部使用的原子值暴露给外界。比如,声明一个全局的原子变量并不是一个正确的做法。这个变量的访问权限最起码也应该是包级私有的。

如果不得不让包外,或模块外的代码使用你的原子值,那么可以声明一个包级私有的原子变量,然后再通过一个或多个公开的函数,让外界间接地使用到它。注意,这种情况下不要把原子值传递到外界,不论是传递原子值本身还是它的指针值。

如果通过某个函数可以向内部的原子值存储值的话,那么就应该在这个函数中先判断被存储值类型的合法性。若不合法,则应该直接返回对应的错误值,从而避免 panic 的发生。

如果可能的话,我们可以把原子值封装到一个数据类型中,比如一个结构体类型。这样,我们既可以通过该类型的方法更加安全地存储值,又可以在该类型中包含可存储值的合法类型信息。

除了上述使用建议之外,我还要再特别强调一点:尽量不要向原子值中存储引用类型的值。因为这很容易造成安全漏洞。请看下面的代码:

var box6 atomic.Value
v6 := []int{1, 2, 3}
box6.Store(v6)
v6[1] = 4 // 注意,此处的 *** 作不是并发安全的!

我把一个[]int类型的切片值v6, 存入了原子值box6。注意,切片类型属于引用类型。所以,我在外面改动这个切片值,就等于修改了box6中存储的那个值。这相当于绕过了原子值而进行了非并发安全的 *** 作。那么,应该怎样修补这个漏洞呢?可以这样做:

store := func(v []int) {
   replica := make([]int, len(v))
   copy(replica, v)
   box6.Store(replica)
}
store(v6)
v6[2] = 5 // 此处的 *** 作是安全的

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996351.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存