golang包的日常(5)——sync包:并发同步;sync/atomic包:原子级内存操作

package sync

import "sync"

sync包提供了基本的同步基元,如互斥锁。除了OnceWaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。

注意:本包的类型的值不应被拷贝。

sync.WaitGroup

type WaitGroup struct {
    // 包含隐藏或非导出字段
}

WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。
WaitGroup对象内部有一个计数器,最初从0开始,它有三个方法:Add, Done, Wait 用来控制计数器的值:

  • func (wg *WaitGroup) Add(delta int)Add方法向内部计数加上deltadelta可以是负数;如果内部计数器变为0Wait方法阻塞等待的所有线程都会释放,如果计数器小于0,方法panic
  • func (wg *WaitGroup) Done()Done方法使WaitGroup计数器的值减1,应在线程的最后执行;
  • func (wg *WaitGroup) Wait()Wait方法阻塞直到WaitGroup计数器减为0

示例:

func f(wg *sync.WaitGroup) {
	defer wg.Done()
	time.Sleep(time.Second)
	fmt.Printf("我是f\n")
}

func main() {
	wg := sync.WaitGroup{}
	wg.Add(1)
	go f(&wg)
	fmt.Println("我是main")
	wg.Wait()
}

示例中WaitGroup对象可以阻塞main函数,直到所有goroutine都运行完毕。
WaitGroup对象是结构体类型,传参时需要使用地址,否则进程会死锁;也可以直接声明全局变量。

sync.Mutex:互斥锁

type Mutex struct {
    // 包含隐藏或非导出字段
}

Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁。

Mutex对象有两个方法:

  • func (m *Mutex) Lock()Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁;
  • func (m *Mutex) Unlock()Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。

先举一个不加互斥锁的例子:

var x int64
var wg sync.WaitGroup

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

运行后会发现输出不是10000,而且无法预测。这是因为两个goroutine可能同时取到x,最后写入时相当于只进行了一次操作,导致结果出错。
对共享资源有修改操作时,可以使用互斥锁。在上例中加入互斥锁:

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock()   // 在访问共享资源前上锁
		x = x + 1     // 上锁后其它goroutine无法访问x
		lock.Unlock() // 访问完就解锁
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

使用互斥锁后输出正确结果10000。
Mutex对象是结构体类型,如果不使用全局变量,那么传参时需要使用地址。

sync.RWMutex:读写互斥锁

type RWMutex struct {
    // 包含隐藏或非导出字段
}

RWMutex是读写互斥锁。该锁可以被同时多个读取者持有或唯一个写入者持有。RWMutex可以创建为其他结构体的字段;零值为解锁状态。RWMutex类型的锁也和线程无关,可以由不同的线程加读取锁/写入锁和解读取锁/写入锁。
当一个goroutine获取读取锁之后,其他的goroutine如果是获取读取锁会继续获得锁,如果是获取写入锁就会等待;当一个goroutine获取写入锁之后,其他的goroutine无论是获取读取锁还是写入锁都会等待。

RWMutex对象有 5 个方法:

  • func (rw *RWMutex) Lock()Lock方法将rw锁定为写入状态,禁止其他线程读取或者写入;
  • func (rw *RWMutex) Unlock()Unlock方法解除rw的写入锁状态,如果rw未加写入锁会导致运行时错误;
  • func (rw *RWMutex) RLock()RLock方法将rw锁定为读取状态,禁止其他线程写入,但不禁止读取;
  • func (rw *RWMutex) RUnlock()Runlock方法解除rw的读取锁状态,如果rw未加读取锁会导致运行时错误;
  • func (rw *RWMutex) RLocker() LockerRlocker方法返回一个互斥锁,通过调用rw.Rlockrw.Runlock实现了Locker接口(该接口包含LockUnlock两个方法)。

读写互斥锁的使用方式和互斥锁相同。对于读多写少的场景,如果使用互斥锁,那么共享资源始终只能被一个goroutine访问,而读写互斥锁可以让多个只读取共享资源的goroutine同时访问共享资源,这在读多写少场景中能大大节省时间。

sync.Once:保证函数只执行一次

type Once struct {
    done uint32 // 表示函数是否已执行
	m    Mutex  // 执行函数时需要上锁,保证只执行一次
}

Once是只执行一次动作的对象。只有一个方法Do

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

Do方法当且仅当第一次被调用时才执行函数f。当Once对象已经调用Do方法后,即使更换f函数也不会再执行,所以不同的f函数需要声明不同的Once对象。
因为f是没有参数的,如果要执行的f需要传递参数就需要搭配闭包来使用,比如:

config.once.Do(func() { config.init(filename) })

因为只有f返回后Do方法才会返回,所以f若引起了Do的调用,会导致死锁。

示例:

var once sync.Once
onceBody := func() {
    fmt.Println("Only once")
}
done := make(chan bool)
for i := 0; i < 10; i++ {
    go func() {
        once.Do(onceBody)
        done <- true
    }()
}
for i := 0; i < 10; i++ {
    <-done
}

输出:

Only once

sync.Once内部包含的互斥锁能保证f的操作是并发安全的。

sync.Map:并发安全的映射

type Map struct {
    // 包含隐藏或非导出字段
}

Map类似于Go map[interface{}]interface{},但是可以安全地被多个goroutine并发使用,而无需额外的锁定或协调。 加载,存储和删除以分摊的常数时间运行。

Map类型是专用的。 大部分代码应改用带有单独锁定或协调功能的普通map,以提高类型安全性,并使其更易于维护其他不变量以及映射内容。

Map类型针对两种常见用例进行了优化:(1)一个键值对仅写入一次但读取多次,例如在仅增长的cache中;(2)多个goroutine进行读取,写入和覆盖不相交的键值对。在这两种情况下,与搭配单独的Mutex或RWMutex的Go map相比,使用Map可以显著减少锁竞争(Lock Contention)。

Map对象无需初始化,声明后即可直接使用。

Map对象目前有6个方法:

  • func (m *Map) Delete(key interface{})Delete方法删除key对应的值;
  • func (m *Map) Load(key interface{}) (value interface{}, ok bool)Load方法返回key对应的值,如果找不到则返回nilok表示是否找到值;
  • func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)LoadAndDelete方法1.15版本才有,其删除key对应的值并返回该值,loaded表示key是否存在;
  • func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)LoadOrStore方法,如果key存在则返回对应的值,如果不存在,则存储传入的keyvalue,返回传入的valueloadedtrue表示载入(load),为false表示存储(store);
  • func (m *Map) Range(f func(key, value interface{}) bool)Range方法遍历Map对象,函数f接收参数keyvalue,返回一个bool值,返回true表示直接进行下一循环(接收下一对key-value),返回false表示结束循环(停止遍历)。遍历过程是无序的,与存储的顺序无关;
  • func (m *Map) Store(key, value interface{})Store方法存储键值对。

Range方法示例:

var m1 sync.Map
m1.Store(1, 1)
m1.Store(2, 2)
m1.Store(3, 3)
f := func(k, v interface{}) bool {
	fmt.Println(k, v)
	if k == 3 {
		return false
	}
	return true
}
m1.Range(f)

输出:

2 2
3 3

因为遍历是无序的,所以输出只是其中一次输出,可以看到遍历到3就结束了。

package atomic

import "sync/atomic"

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法,它在用户态就可以完成,因此性能比加锁操作更好。

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。

这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

应通过通信来共享内存,而不通过共享内存实现通信。

atomic包提供的函数如下:

读取系列

原子性的获取*addr的值。

  • func LoadInt32(addr *int32) (val int32)
  • func LoadInt64(addr *int64) (val int64)
  • func LoadUint32(addr *uint32) (val uint32)
  • func LoadUint64(addr *uint64) (val uint64)
  • func LoadUintptr(addr *uintptr) (val uintptr)
  • func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

写入系列

原子性的将val的值保存到*addr

  • func StoreInt32(addr *int32, val int32)
  • func StoreInt64(addr *int64, val int64)
  • func StoreUint32(addr *uint32, val uint32)
  • func StoreUint64(addr *uint64, val uint64)
  • func StoreUintptr(addr *uintptr, val uintptr)
  • func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

修改系列(加减操作)

原子性的将delta的值添加到*addr并返回新值。

  • func AddInt32(addr *int32, delta int32) (new int32)
  • func AddInt64(addr *int64, delta int64) (new int64)
  • func AddUint32(addr *uint32, delta uint32) (new uint32)
  • func AddUint64(addr *uint64, delta uint64) (new uint64)
  • func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

特别的,对于AddUint32AddUint64,以AddUint64为例,如果要让x减去一个值c,调用AddUint64(&x, ^uint64(c-1));让x减1,调用AddUint64(&x, ^uint64(0))

交换系列

原子性的将new值保存到*addr并返回*addr原来的值。

  • func SwapInt32(addr *int32, new int32) (old int32)
  • func SwapInt64(addr *int64, new int64) (old int64)
  • func SwapUint32(addr *uint32, new uint32) (old uint32)
  • func SwapUint64(addr *uint64, new uint64) (old uint64)
  • func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
  • func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

比较并交换系列

原子性的比较*addrold,如果相同则将new赋值给*addr并返回真。

  • func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
  • func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
  • func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
  • func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
  • func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
  • func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

参考

标准库中文文档
标准库英文文档/sync包
博客1
博客2

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值