Go语言核心36讲(Go语言实战与应用八)--学习笔记

2021年11月22日 阅读数:2
这篇文章主要向大家介绍Go语言核心36讲(Go语言实战与应用八)--学习笔记,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

30 | 原子操做(下)

咱们接着上一篇文章的内容继续聊,上一篇咱们提到了,sync/atomic包中的函数能够作的原子操做有:加法(add)、比较并交换(compare and swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。而且以此衍生出了两个问题。git

今天咱们继续来看第三个衍生问题: 比较并交换操做与交换操做相比有什么不一样?优点在哪里?github

回答是:比较并交换操做即 CAS 操做,是有条件的交换操做,只有在条件知足的状况下才会进行值的交换。安全

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

在进行 CAS 操做的时候,函数会先判断被操做变量的当前值,是否与咱们预期的旧值相等。若是相等,它就把新值赋给该变量,并返回true以代表交换操做已进行;不然就忽略交换操做,并返回false。函数

能够看到,CAS 操做并非单一的操做,而是一种操做组合。这与其余的原子操做都不一样。正由于如此,它的用途要更普遍一些。例如,咱们将它与for语句联用就能够实现一种简易的自旋锁(spinlock)。atom

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

在for语句中的 CAS 操做能够不停地检查某个须要知足的条件,一旦条件知足就退出for循环。这就至关于,只要条件未被知足,当前的流程就会被一直“阻塞”在这里。指针

这在效果上与互斥锁有些相似。不过,它们的适用场景是不一样的。咱们在使用互斥锁的时候,老是假设共享资源的状态会被其余的 goroutine 频繁地改变。code

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

package main

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

func main() {
	// 第三个衍生问题的示例。
	forAndCAS1()
	fmt.Println()
	forAndCAS2()
}

// 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
}

// forAndCAS2 用于展现一种简易的(且更加宽松的)互斥锁的模拟。
func forAndCAS2() {
	sign := make(chan struct{}, 2)
	num := int32(0)
	fmt.Printf("The number: %d\n", num)
	max := int32(20)
	go func(id int, max int32) { // 定时增长num的值。
		defer func() {
			sign <- struct{}{}
		}()
		for i := 0; ; i++ {
			currNum := atomic.LoadInt32(&num)
			if currNum >= max {
				break
			}
			newNum := currNum + 2
			time.Sleep(time.Millisecond * 200)
			if atomic.CompareAndSwapInt32(&num, currNum, newNum) {
				fmt.Printf("The number: %d [%d-%d]\n", newNum, id, i)
			} else {
				fmt.Printf("The CAS operation failed. [%d-%d]\n", id, i)
			}
		}
	}(1, max)
	go func(id int, max int32) { // 定时增长num的值。
		defer func() {
			sign <- struct{}{}
		}()
		for j := 0; ; j++ {
			currNum := atomic.LoadInt32(&num)
			if currNum >= max {
				break
			}
			newNum := currNum + 2
			time.Sleep(time.Millisecond * 200)
			if atomic.CompareAndSwapInt32(&num, currNum, newNum) {
				fmt.Printf("The number: %d [%d-%d]\n", newNum, id, j)
			} else {
				fmt.Printf("The CAS operation failed. [%d-%d]\n", id, j)
			}
		}
	}(2, max)
	<-sign
	<-sign
}

第四个衍生问题:假设我已经保证了对一个变量的写操做都是原子操做,好比:加或减、存储、交换等等,那我对它进行读操做的时候,还有必要使用原子操做吗?接口

回答是:颇有必要。其中的道理你能够对照一下读写锁。为何在读写锁保护下的写操做和读操做之间是互斥的?这是为了防止读操做读到没有被修改完的值,对吗?

若是写操做尚未进行完,读操做就来读了,那么就只能读到仅修改了一部分的值。这显然破坏了值的完整性,读出来的值也是彻底错误的。

因此,一旦你决定了要对一个共享资源进行保护,那就要作到彻底的保护。不彻底的保护基本上与不保护没有什么区别。

好了,上面的主问题以及相关的衍生问题涉及了原子操做函数的用法、原理、对比和一些最佳实践,但愿你已经理解了。

因为这里的原子操做函数只支持很是有限的数据类型,因此在不少应用场景下,互斥锁每每是更加适合的。

不过,一旦咱们肯定了在某个场景下可使用原子操做函数,好比:只涉及并发地读写单一的整数类型值,或者多个互不相关的整数类型值,那就不要再考虑互斥锁了。

这主要是由于原子操做函数的执行速度要比互斥锁快得多。并且,它们使用起来更加简单,不会涉及临界区的选择,以及死锁等问题。固然了,在使用 CAS 操做的时候,咱们仍是要多加注意的,由于它能够被用来模仿锁,并有可能“阻塞”流程。

知识扩展

问题:怎样用好sync/atomic.Value?

为了扩大原子操做的适用范围,Go 语言在 1.4 版本发布的时候向sync/atomic包中添加了一个新的类型Value。此类型的值至关于一个容器,能够被用来“原子地”存储和加载任意的值。

atomic.Value类型是开箱即用的,咱们声明一个该类型的变量(如下简称原子变量)以后就能够直接使用了。这个类型使用起来很简单,它只有两个指针方法:Store和Load。不过,虽然简单,但仍是有一些值得注意的地方的。

首先一点,一旦atomic.Value类型的值(如下简称原子值)被真正使用,它就不该该再被复制了。什么叫作“真正使用”呢?

咱们只要用它来存储值了,就至关于开始真正使用了。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 // 此处的操做是安全的。

我先为切片值v6建立了一个彻底的副本。这个副本涉及的数据已经与原值绝不相干了。而后,我再把这个副本存入box6。如此一来,不管我再对v6的值作怎样的修改,都不会破坏box6提供的安全保护。

以上,就是我要告诉你的关于atomic.Value的注意事项和使用建议。你能够在 demo64.go 文件中看到相应的示例。

package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"sync/atomic"
)

func main() {
	// 示例1。
	var box atomic.Value
	fmt.Println("Copy box to box2.")
	box2 := box // 原子值在真正使用前能够被复制。
	v1 := [...]int{1, 2, 3}
	fmt.Printf("Store %v to box.\n", v1)
	box.Store(v1)
	fmt.Printf("The value load from box is %v.\n", box.Load())
	fmt.Printf("The value load from box2 is %v.\n", box2.Load())
	fmt.Println()

	// 示例2。
	v2 := "123"
	fmt.Printf("Store %q to box2.\n", v2)
	box2.Store(v2) // 这里并不会引起panic。
	fmt.Printf("The value load from box is %v.\n", box.Load())
	fmt.Printf("The value load from box2 is %q.\n", box2.Load())
	fmt.Println()

	// 示例3。
	fmt.Println("Copy box to box3.")
	box3 := box // 原子值在真正使用后不该该被复制!
	fmt.Printf("The value load from box3 is %v.\n", box3.Load())
	v3 := 123
	fmt.Printf("Store %d to box3.\n", v3)
	//box3.Store(v3) // 这里会引起一个panic,报告存储值的类型不一致。
	_ = box3
	fmt.Println()

	// 示例4。
	var box4 atomic.Value
	v4 := errors.New("something wrong")
	fmt.Printf("Store an error with message %q to box4.\n", v4)
	box4.Store(v4)
	v41 := io.EOF
	fmt.Println("Store a value of the same type to box4.")
	box4.Store(v41)
	v42, ok := interface{}(&os.PathError{}).(error)
	if ok {
		fmt.Printf("Store a value of type %T that implements error interface to box4.\n", v42)
		//box4.Store(v42) // 这里会引起一个panic,报告存储值的类型不一致。
	}
	fmt.Println()

	// 示例5。
	box5, err := NewAtomicValue(v4)
	if err != nil {
		fmt.Printf("error: %s\n", err)
	}
	fmt.Printf("The legal type in box5 is %s.\n", box5.TypeOfValue())
	fmt.Println("Store a value of the same type to box5.")
	err = box5.Store(v41)
	if err != nil {
		fmt.Printf("error: %s\n", err)
	}
	fmt.Printf("Store a value of type %T that implements error interface to box5.\n", v42)
	err = box5.Store(v42)
	if err != nil {
		fmt.Printf("error: %s\n", err)
	}
	fmt.Println()

	// 示例6。
	var box6 atomic.Value
	v6 := []int{1, 2, 3}
	fmt.Printf("Store %v to box6.\n", v6)
	box6.Store(v6)
	v6[1] = 4 // 注意,此处的操做不是并发安全的!
	fmt.Printf("The value load from box6 is %v.\n", box6.Load())
	// 正确的作法以下。
	v6 = []int{1, 2, 3}
	store := func(v []int) {
		replica := make([]int, len(v))
		copy(replica, v)
		box6.Store(replica)
	}
	fmt.Printf("Store %v to box6.\n", v6)
	store(v6)
	v6[2] = 5 // 此处的操做是安全的。
	fmt.Printf("The value load from box6 is %v.\n", box6.Load())
}

type atomicValue struct {
	v atomic.Value
	t reflect.Type
}

func NewAtomicValue(example interface{}) (*atomicValue, error) {
	if example == nil {
		return nil, errors.New("atomic value: nil example")
	}
	return &atomicValue{
		t: reflect.TypeOf(example),
	}, nil
}

func (av *atomicValue) Store(v interface{}) error {
	if v == nil {
		return errors.New("atomic value: nil value")
	}
	t := reflect.TypeOf(v)
	if t != av.t {
		return fmt.Errorf("atomic value: wrong type: %s", t)
	}
	av.v.Store(v)
	return nil
}

func (av *atomicValue) Load() interface{} {
	return av.v.Load()
}

func (av *atomicValue) TypeOfValue() reflect.Type {
	return av.t
}

总结

咱们把这两篇文章一块儿总结一下。相对于原子操做函数,原子值类型的优点很明显,但它的使用规则也更多一些。首先,在首次真正使用后,原子值就不该该再被复制了。

其次,原子值的Store方法对其参数值(也就是被存储值)有两个强制的约束。一个约束是,参数值不能为nil。另外一个约束是,参数值的类型不能与首个被存储值的类型不一样。也就是说,一旦一个原子值存储了某个类型的值,那它之后就只能存储这个类型的值了。

基于上面这几个注意事项,我提出了几条使用建议,包括:不要对外暴露原子变量、不要传递原子值及其指针值、尽可能不要在原子值中存储引用类型的值,等等。与之相关的一些解决方案我也一并提出了。但愿你可以受用。

原子操做明显比互斥锁要更加轻便,可是限制也一样明显。因此,咱们在进行二选一的时候一般不会太困难。可是原子值与互斥锁之间的选择有时候就须要仔细的考量了。不过,若是你能牢记我今天讲的这些内容的话,应该会有很大的助力。

思考题

今天的思考题只有一个,那就是:若是要对原子值和互斥锁进行二选一,你认为最重要的三个决策条件应该是什么?

笔记源码

https://github.com/MingsonZheng/go-core-demo

知识共享许可协议

本做品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、从新发布,但务必保留文章署名 郑子铭 (包含连接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。