Go中经典同步工具

把经典的同步工具总结一下

互斥锁(sync.Mutex)

  • 加锁:Lock()
    解锁:Unlock()
  • 全局锁,加锁后未解锁之前再次加锁会panic,解锁依然
    入参是不是指针类型都可以
package main

import(
	"fmt"
	"time"
	"sync"
	"math/rand"
)
//互斥锁(全局锁)不确定读写时
var lock sync.Mutex

func main() {

	testMap()
}
func testMap() {
	var a map[int]int
	a = make(map[int]int, 5)

	a[8] = 10
	a[3] = 10
	a[2] = 10
	a[1] = 10
	a[18] = 10

	for i := 0; i < 2; i++ {
		go func(b map[int]int) {
			lock.Lock()
			b[8] = rand.Intn(100)
			lock.Unlock()
		}(a)
	}

	lock.Lock()
	fmt.Println(a)
	lock.Unlock()

	time.Sleep(time.Second)
	fmt.Println(a)
}

读写锁(sync.RWMutex)

  • 写锁加锁:sync.Lock
    写锁解锁:sync.Unlock
  • 读锁加锁:sync.RLock
    读锁解锁:sync.RUnlock
    -返回Locker接口:RLocker
    Locker接口中有lock()和Unlock(),无论是互斥锁还是读写锁都实现接口中的方法
  • 入参是不是指针类型都可以,可以同时多个读锁,但是写锁只能有一个,而且两个锁不能同时开.
    除非先开读锁,再开写锁,否则panic
  • sync.WaitGroup必须是指针类型保证唯一
package main

import (
	"fmt"
	"sync"
)
//读写锁
func main() {
	var rw sync.RWMutex
	var wg sync.WaitGroup
	wg.Add(2)
	go read(&rw,1,&wg)
	go read(&rw,2,&wg)
	wg.Wait()
}
//wg *sync.WaitGroup必须是地址传递,保证是同一个对象
func read(rw *sync.RWMutex,i int,wg *sync.WaitGroup)  {
	fmt.Println(i,"开始读...")
	rw.RLock()
	fmt.Println(i,"正在读...")
	rw.RUnlock()
	fmt.Println(i,"结束读...")
	wg.Done()
}

条件变量(sync.Cond)

使用方法:

  • 1 使用cond := sync.NewCond()来创建,入参必须是Locker类型,参考用例
  • 2 传递cond时必须是引用,或者取地址,千万不要值传递
  • 3 然后加锁,使用wait()等待通知,最后再解锁,wait最好在for循环里面操作.
  • 4 使用Signal()和Broadcast()方法发送通知

粗浅原理:

  • sync.Cond源码结构
type Cond struct {
    // noCopy可以嵌入到结构中,在第一次使用后不可复制,使用go vet作为检测使用
	noCopy noCopy 
	// 根据需求初始化不同的锁,如*Mutex 和 *RWMutex
	L Locker
	 // 通知列表,调用Wait()方法的goroutine会被放入list中,每次唤醒,从这里取出
	notify  notifyList 
	// 复制检查,检查cond实例是否被复制
	checker copyChecker 
}
  • 当我们创建了cond的实例以后,调用wait方法,在wait
    中会先对cond检查是不是值传递,然后将当前所在goroutine方法notifyList(通知列表中),接着加锁
    Wait()函数源码如下:

func (c *Cond) Wait() {
    // 检查c是否是被复制的,如果是就panic
	c.checker.check()
	// 将当前goroutine加入等待队列
	t := runtime_notifyListAdd(&c.notify)
	// 解锁
	c.L.Unlock()
	// 等待队列中的所有的goroutine执行等待唤醒操作
	runtime_notifyListWait(&c.notify, t)
	c.L.Lock()
}
  • 两个通知函数的源码
func (c *Cond) Signal() {
    // 检查c是否是被复制的,如果是就panic
	c.checker.check()
	// 通知等待列表中的一个 
	runtime_notifyListNotifyOne(&c.notify)
}

func (c *Cond) Broadcast() {
    // 检查c是否是被复制的,如果是就panic
	c.checker.check()
	// 唤醒等待队列中所有的goroutine
	runtime_notifyListNotifyAll(&c.notify)
}

疑问
这里摘抄了郝大哥的Go语言核心36讲中的两个问题:

  • 为什么先要锁定条件变量基于的互斥锁,才能调用它的Wait方法?

因为条件变量的Wait方法在阻塞当前的 goroutine 之前,会解锁它基于的互斥锁,所以在调用该Wait方法之前,我们必须先锁定那个互斥锁,否则在调用这个Wait方法时,就会引发一个不可恢复的 panic。
为什么条件变量的Wait方法要这么做呢?你可以想象一下,如果Wait方法在互斥锁已经锁定的情况下,阻塞了当前的 goroutine,那么又由谁来解锁呢?别的 goroutine 吗?
先不说这违背了互斥锁的重要使用原则,即:成对的锁定和解锁,就算别的 goroutine 可以来解锁,那万一解锁重复了怎么办?由此引发的 panic 可是无法恢复的。
如果当前的 goroutine 无法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?只要共享资源的状态不变,即使当前的 goroutine 因收到通知而被唤醒,也依然会再次执行这个Wait<方法,并再次被阻塞。
所以说,如果条件变量的Wait方法不先解锁互斥锁的话,那么就只会造成两种后果:不是当前的程序因 panic 而崩溃,就是相关的 goroutine 全面阻塞。

  • 为什么要用for语句来包裹调用其Wait方法的表达式,用if不可以吗?

显然,if语句只会对共享资源的状态检查一次,而for语句却可以做多次检查,直到这个状态改变为止。那为什么要做多次检查呢?
这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。
这种情况是很有可能发生的

测试例子

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var locker = new(sync.Mutex)
	cond := sync.NewCond(locker)//cond最后返回的必须是指针,入参也要是指针
	var wg sync.WaitGroup

	for i:=0;i<40;i++ {
		wg.Add(1)
		go func(x int) {
			cond.L.Lock()
			defer cond.L.Unlock()
			cond.Wait()
			fmt.Println("我等到了通知,我输出:",x)
			wg.Done()
		}(i)
	}

	time.Sleep(time.Second)
	fmt.Println("我现在通知其中一个........")
	cond.Signal()
	time.Sleep(time.Second)
	fmt.Println("我再次通知其中一个........")
	cond.Signal()
	time.Sleep(time.Second)
	fmt.Println("我通知所有的..........")
	cond.Broadcast()
	wg.Wait()
}

原子操作

重点

  • 变量类型从头到尾一致,这也是与锁区别的其中一点

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {

	i32 := new(int32)

	//加法操作 向地址中原值做加法操作,并且返回新值
	atomic.AddInt32(i32, 32)
	addInt := atomic.AddInt32(i32, 2)
	fmt.Println("addInt : ", addInt)

	//保存指针的类型做加法操作
	uiptr := new(uintptr)
	uiptrval := atomic.AddUintptr(uiptr, 12)
	uiptrval = atomic.AddUintptr(uiptr, 23)
	fmt.Println("uiptrval : ", uiptrval)

	//减法操作 u开头的类型不能做减法操作
	addInt = atomic.AddInt32(i32, -32)
	fmt.Println("addInt : ", addInt)

	//CAS 交换值 old值必须和地址中的旧值一样
	bool := atomic.CompareAndSwapInt32(i32, 2, 23)
	fmt.Println("bool : ", bool)
	fmt.Println("addInt : ", *i32)

	//获取值 将地址中的值取出来
	addInt = atomic.LoadInt32(i32)
	fmt.Println("addInt : ", addInt)

	//存值或者更改值
    atomic.StoreInt32(i32,100)
	fmt.Println("StoreInt32 : ", *i32)

	//存值或者更改值 并把旧值返回
	oldInt := atomic.SwapInt32(i32,200)
	fmt.Println("oldInt : ", oldInt)

	//原子值 可以放任意类型 但是不能放置nil类型
	var atomicValue atomic.Value
	sli := []string{"1","2","3"}
	atomicValue.Store(sli)//存储进去,这样存储进去是不安全的,切片是引用传递,可以在外面更改切片的值,可以拷贝一份再放进去
	//更改他
	sli[0] = "更改他"
	fmt.Println("已经被更改",atomicValue.Load())
	//复制一份
	copySlice := make([]string,len(sli))
	copy(copySlice,sli)
	atomicValue.Store(copySlice)
	//更改他 发现无法被更改回来.
	sli[0] = "1"
	fmt.Println(atomicValue.Load())

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值