Go语言资源互斥——互斥锁/读写互斥锁/sync.Once()/atomic/sync.Map

sync包

互斥锁

互斥锁是一种常见的控制资源访问的方法,它可以保证同时只有一个goroutine可以访问临界资源。Go语言中可以使用sync包的Mutex类型来实现互斥锁。

例:

// 开启多个gorourtine执行add()操作,会导致两个goroutine读取到相同的x导致最终结果偏小
var x = 0
var wg sync.WaitGroup

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

func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	println(x)
}

改进:

var lock sync.Mutex
func add() {
   defer wg.Done()
   for i := 0; i < 5000; i++{
      lock.Lock()  // 加锁
      x += 1
      lock.Unlock()  // 解锁
   }
}

使用互斥锁能够保证同一时间有且仅有一个goroutine进入临界区,其他的goroutine则在等待锁。当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,等待的策略是随机的。

读写互斥锁

互斥锁是完全互斥的,但是很多实际的场景下是读多写少的,当我们并发的读取一个资源不涉及资源修改时是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包的RWMutex类型。

读写锁分为两种:读锁与写锁。当一个goroutine获取读写锁之后,其他的goroutine如果是获取读锁会继续加锁,如果是获取写锁就会继续等待;当一个goroutine获取写锁后,其他的goroutine无论是读锁还是写锁都会继续等待。

package main

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

var (
   x int
   wg sync.WaitGroup
   //lock sync.Mutex
   rwLock sync.RWMutex
)

func write()  {
   defer wg.Done()
   // lock.Lock()
   rwLock.Lock()  // 加写锁
   x += 1
   time.Sleep(time.Millisecond * 10)  // 模拟写操作
   rwLock.Unlock()  // 解写锁
   // lock.Unlock()  //
}

func read() {
   defer wg.Done()
   //lock.Lock()  // 加互斥锁
   rwLock.RLock()  // 加读锁
   time.Sleep(time.Millisecond)  // 模拟读操作
   rwLock.RUnlock()  // 解写锁
   // lock.Unlock()
}

func main() {
   start := time.Now()
   for i := 0; i < 10; i++{
      wg.Add(1)
      go write()
   }

   for i := 0; i < 1000; i++{
      wg.Add(1)
      go read()
   }
   wg.Wait()

   end := time.Now()
   fmt.Println(end.Sub(start))
}

读写锁适合读多写少的应用场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来。

sync.Once()

在编程的很多场景下需要确保某些操作在高并发场景下之执行一次,例如只加载一次配置文件,只关闭一次通道等。

Go语言中sync提供了一个针对只执行一次场景的解决方案——sync.Once

sync.Once只有一个Do方法,签名:

func (o *Once) Do(f func()){}

如果要执行的函数f需要传递参数需要搭配闭包来使用。

加载配置文件示例:

延迟一个开销很大的初始化操作到真正使用它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如再init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须的。

var icons map[string]image.Image

func loadIcons(){
    icons = map[string]image.Image{
        "left": loadIcon("left.png"),
        "up": loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down": loadIcon("down.png"),
    }
}

// Icon被多个goroutine调用时不是并发安全
func Icon(name string) image.Image{
    if icons == nil{
        loadIcons()
    }
    return icons[name]
}

多个goroutine并发调用Icon函数不是并发安全的,现代编译器和CPU可能会在保证每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重拍为以下结果:

func loadIcons(){
    icon = make(map[string]image.Image)
    icons["left"] = loadIcon("left.png")
    icons["up"] = loadIcon("up.png")
    icons["right"] = loadIcon("right.png")
    icons["down"] = loadIcon("down.png")
}

在这种情况下就会出现即使判断力icons不为nil也不意味着变量初始化完成了。因此,考虑添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但这又引发性能问题。

使用sync.Once改造的实例代码:

var icons map[string]image.Image
var loadIconOnce sync.Once

func loadIcons(){
    icons = map[string]image.Image{
        "left": loadIcon("left.png"),
        "up": loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down": loadIcon("down.png"),
    }
}

// Icon 并发安全
func Icon(name string) image.Image{
    loadIconOnce.Do(loadIcons)
    return icons[name]
}

sync.Once内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计保证初始化操作时是并发安全且初始化操作时不会被执行多次。

sync.Map

Go语言中内置的map不是并发安全的,当仅开启少量几个goroutine的时候可能没什么问题,当并发多了之后会导致fatal error: concurrent map writes错误。

因此需要为map保证并发安全性,Go语言sync提供了并发安全版map——sync.Map,其不用像内置map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

var m sync.Map

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 30; i++{
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)
			value, _ := m.Load(key)
			fmt.Printf("k = %v, v := %v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}
atomic

互斥与原子操作

package main

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

// 原子操作
var x int64
var wg sync.WaitGroup
//var lock sync.Mutex

func add() {
	defer wg.Done()
	//lock.Lock()
	atomic.AddInt64(&x, 1)
	//x++
	//lock.Unlock()
}

func main() {
	wg.Add(100000)
	for i := 0; i < 100000; i++{
		go add()
	}
	wg.Wait()
	fmt.Println(x)
}

atomic提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正常使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步比较好。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值