Golang中,有哪些常见的数据结构是线程安全的?

2,310 阅读5分钟

在实际项目中,线程安全的问题肯定会涉及到,这篇文章就总结Golang中有哪些常见的数据结构是线程安全的,以及他们的使用场景。

常见数据结构:

  1. sync.Mutex:这是一种互斥锁,可以用来保护对共享数据的访问。使用时,需要在访问共享数据的代码块之前调用 Lock 方法,在代码块执行完毕后调用 Unlock 方法。这是Golang中最基本的悲观锁,很多的数据结构都是通过sync.Mutex来实现线程安全。
  2. chan:这是 Go 中的通道,可以在多个 goroutine 之间进行数据传递。在通道的发送和接收操作中,Go 会自动进行加锁,保证线程安全,可以从goroutine的源码中看出,其结构中有lock的字段。
type hchan struct {
	qcount   uint           // 循环数组中的数量
	dataqsiz uint           // 循环数组中的size
    // channel分为有缓冲和无缓冲两种
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16	// channel中的元素大小
	closed   uint32  // channel是否关闭
	elemtype *_type // channel中的元素类型
	sendx    uint   // 循环数组中的下一次发送下标位置
	recvx    uint   // 循环数组中的下一次接受下标位置
    // 尝试读取channel或向channel写入数据而被阻塞的goroutine
    // waitq 是一个双向链表
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}
  1. sync.RWMutex:这是一种读写锁,可以用来保护对共享数据的访问。与互斥锁不同的是,读写锁允许多个 goroutine 同时读取共享数据,但在写入时会阻塞读取操作。
type RWMutex struct {
	w           Mutex  // 互斥锁用于保证写操作的独占访问
	writerSem   uint32 // 保护写操作的独占访问,同时也用于记录当前有多少个写操作正在进行。
	readerSem   uint32 // 保护读操作的独占访问,同时也用于记录当前有多少个读操作正在进行。
	readerCount int32  // 计数器,用于记录当前有多少个读操作正在进行
	readerWait  int32  // 计数器,用于记录写操作阻塞时的读操作数量
}

在获取读锁时,RWMutex 会先使用互斥锁保护计数器,并将计数器加 1。如果当前没有写操作正在进行,则会立即返回;否则,RWMutex会使用互斥锁将当前 goroutine 阻塞,直到所有写操作完成为止。在释放读锁时RWMutex会再次使用互斥锁保护计数器,并将计数器减 1。如果计数器减为 0,则表明当前没有读操作正在进行,RWMutex会唤醒所有被阻塞的写操作。在获取写锁时,RWMutex 会使用互斥锁阻塞所有的读操作和写操作,直到当前写操作完成为止。

  1. sync.Once:这是一种用于保证某段代码仅执行一次的工具。使用时,可以通过调用 Do 方法来执行指定的代码块,如果之前已经调用过 Do 方法,那么这次调用就会被忽略。
import "sync"

type singleton struct {
}

var (
    instance *singleton
    once     sync.Once
)

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{
        }
    })
    return instance
}
  1. sync.Map:这是一种并发安全的 map,可以在多个 goroutine 之间安全地读写。底层层实现逻辑和chan一样,也是通过sync.Mutex来实现的。
  2. sync.WaitGroup:这是一种用于等待一组 goroutine 执行完毕的工具。使用时,可以通过调用 Add 方法来指定等待的 goroutine 数量,然后在每个 goroutine 执行完毕后调用 Done 方法来通知 WaitGroup。最后,可以调用 Wait 方法来阻塞当前 goroutine,直到所有等待的 goroutine 执行完毕。

其他的数据结构:

  1. sync.Pool:这是一种对象池,可以用来缓存对象,避免频繁地分配和释放内存。使用时,可以通过调用 Put 方法将对象放入池中,通过调用 Get 方法获取对象。
  2. sync.Cond:这是一种条件变量,可以用来在多个 goroutine 之间同步执行。使用时,可以通过调用 Wait 方法阻塞当前 goroutine,直到满足特定条件时被唤醒,或者通过调用 Signal 方法唤醒一个被阻塞的 goroutine
  3. sync.Locker:这是一个接口,定义了加锁、解锁、尝试加锁的操作。实现了这个接口的类型都是线程安全的。

虽然这些数据结构是线程安全的,但使用它们时仍需要注意一些问题,例如:

  • 避免死锁:使用互斥锁时,要注意避免两个 goroutine 之间相互等待对方释放锁,从而导致死锁。
  • 尽量减少加锁时间:加锁会影响性能,应尽量减少加锁时间。
  • 使用适当的锁类型:应根据需要选择适当的锁类型,例如读多写少时可以使用读写锁,避免写操作阻塞读操作。

在 Go 中,还有一些数据结构是默认是非线程安全的,例如:

  1. map:这是 Go 中的内置 map 类型,默认是非线程安全的。如果需要在多个 goroutine 之间安全地读写 map,可以使用 sync.Map 或自己实现加锁机制。
  2. slice:这是 Go 中的内置 slice 类型,默认是非线程安全的。如果需要在多个 goroutine 之间安全地读写 slice,可以使用互斥锁或读写锁进行保护。
  3. 结构体:Go 中的结构体默认是非线程安全的。如果需要在多个 goroutine 之间安全地读写结构体,可以使用互斥锁或读写锁进行保护。