目录

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

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

常见数据结构

sync.Mutex :这是一种互斥锁,可以用来保护对共享数据的访问。使用时,需要在访问共享数据的代码块之前调用 Lock 方法,在代码块执行完毕后调用 Unlock 方法。这是 Golang 中最基本的悲观锁,很多的数据结构都是通过 sync.Mutex 来实现线程安全。

chan :这是 Go 中的通道,可以在多个 goroutine 之间进行数据传递。在通道的发送和接收操作中,Go 会自动进行加锁,保证线程安全,可以从 goroutine 的源码中看出,其结构中有 lock 的字段。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
  #+begin_quote
  #+end_quote
  // (in particular, do not ready a G), as this can deadlock
  // with stack shrinking.
  lock mutex
}

sync.RWMutex :这是一种读写锁,可以用来保护对共享数据的访问。与互斥锁不同的是,读写锁允许多个 goroutine 同时读取共享数据,但在写入时会阻塞读取操作。

1
2
3
4
5
6
7
type RWMutex struct {
  w           Mutex  // 互斥锁用于保证写操作的独占访问
  writerSem   uint32 // 保护写操作的独占访问,同时也用于记录当前有多少个写操作正在进行。
  readerSem   uint32 // 保护读操作的独占访问,同时也用于记录当前有多少个读操作正在进行。
  readerCount int32  // 计数器,用于记录当前有多少个读操作正在进行
  readerWait  int32  // 计数器,用于记录写操作阻塞时的读操作数量
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import "sync"

type singleton struct {
}

var (
    instance *singleton
    once     sync.Once
)

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{
        }
    })
    return instance
}

sync.Map :这是一种并发安全的 map,可以在多个 goroutine 之间安全地读写。底层层实现逻辑和 chan 一样,也是通过 sync.Mutex 来实现的。

sync.WaitGroup :这是一种用于等待一组 goroutine 执行完毕的工具。使用时,可以通过调用 Add 方法来指定等待的 goroutine 数量,然后在每个 goroutine 执行完毕后调用 Done 方法来通知 WaitGroup。最后,可以调用 Wait 方法来阻塞当前 goroutine,直到所有等待的 goroutine 执行完毕。

其他的数据结构:

sync.Pool :这是一种对象池,可以用来缓存对象,避免频繁地分配和释放内存。使用时,可以通过调用 Put 方法将对象放入池中,通过调用 Get 方法获取对象。

sync.Cond :这是一种条件变量,可以用来在多个 goroutine 之间同步执行。使用时,可以通过调用 Wait 方法阻塞当前 goroutine,直到满足特定条件时被唤醒,或者通过调用 Signal 方法唤醒一个被阻塞的 goroutine。

sync.Locker :这是一个接口,定义了加锁、解锁、尝试加锁的操作。实现了这个接口的类型都是线程安全的。

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

避免死锁:使用互斥锁时,要注意避免两个 goroutine 之间相互等待对方释放锁,从而导致死锁。

尽量减少加锁时间:加锁会影响性能,应尽量减少加锁时间。

使用适当的锁类型:应根据需要选择适当的锁类型,例如读多写少时可以使用读写锁,避免写操作阻塞读操作。

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

map :这是 Go 中的内置 map 类型,默认是非线程安全的。如果需要在多个 goroutine 之间安全地读写 map,可以使用 sync.Map 或自己实现加锁机制。

slice :这是 Go 中的内置 slice 类型,默认是非线程安全的。如果需要在多个 goroutine 之间安全地读写 slice,可以使用互斥锁或读写锁进行保护。

结构体:Go 中的结构体默认是非线程安全的。如果需要在多个 goroutine 之间安全地读写结构体,可以使用互斥锁或读写锁进行保护。

参考

https://juejin.cn/post/7184308263276511293