Golang并发编程——sync包详解

1 关于sync包

sync包提供了如互斥锁之类的基本同步原语,除了Once和WaitGroup类型外,大多数是供低级库例程使用的。通过通道和通信可以更好地实现更高级别的同步。

包中定义的类型的值,都不应该被复制

2 Cond 条件变量

2.1 关于Cond

Cond实现类一个条件变量,用于gorountines等待或者通知事件发生的集合点。

type Cond struct {
	noCopy noCopy // 标识不能被复制

	// L is held while observing or changing the condition
	L Locker // 每个Cond都有一个关联的Locker L(通常是Mutex或RWMutex),在改变条件和调用Wait方法时必须保持它。

	notify  notifyList // 通知列表
	checker copyChecker //复制检查器
}

2.2 结构体方法

2.2.1 NewCond构造函数

用于构造一个Cond结构体对象,无需多言~

// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
	return &Cond{L: l}
}

2.2.2 Wait等待

Wait方法会以原子地方式解锁c.L并暂停调用者goroutine的执行(进入等待状态);当后续恢复执行时,Wait方法会在返回前锁住c.L

  • 除非被Signal或Broadcast方法唤醒,否则等待的方法不会恢复执行。
func (c *Cond) Wait() {
	c.checker.check() // 检查Cond是否被复制
	t := runtime_notifyListAdd(&c.notify) // 添加通知对象
	c.L.Unlock() // 解锁
	runtime_notifyListWait(&c.notify, t)
	c.L.Lock() // 加锁
}

需要特别注意的是:由于等待时,c.L是没有上锁的,通常情况下,调用者不能假定当Wait方法返回时,等待的条件是满足的,因此调用者应该在循环中等待。

c.L.Lock()
for !condition() {
	    c.Wait()
}
... make use of condition ...
c.L.Unlock()

如果在调用 sync.Cond 类型的 Wait 方法时不在循环中检查条件,可能会导致一些问题,主要是因为条件变量的特性和工作原理:在调用 Wait 方法时,条件变量会释放关联的互斥锁,并阻塞当前协程,直到另一个协程调用 Signal 或 Broadcast 方法来通知条件变量。在通知后,等待的协程会被唤醒,并尝试重新获取互斥锁。如果不在循环中检查条件,等待的协程可能会在条件不满足的情况下被唤醒,然后继续执行后续逻辑,这可能导致程序行为不符合预期。可能出现的问题:

  • 竞态条件:如果在等待协程被唤醒后不再检查条件就继续执行后续逻辑,可能会导致竞态条件的发生,从而导致程序出现不确定的行为。

  • 条件不满足:如果在唤醒后直接执行后续逻辑而不检查条件,可能会导致程序在条件不满足的情况下继续执行,从而产生错误的结果。

  • 协程阻塞:如果不在循环中调用 Wait 方法,等待的协程可能会在条件不满足的情况下被唤醒,但由于条件仍未满足,协程会继续等待,导致协程长时间阻塞。

因此,为了避免这些问题,通常建议在调用 sync.Cond 类型的 Wait 方法时使用循环来检查条件。这样可以确保条件满足时才继续执行后续逻辑,避免竞态条件和不确定行为的发生。

2.2.3 Signal通知

Signal方法会唤醒一个等待的goroutine(如果有的话)。

  • 在调用Signal方法时,允许调用者持有c.L锁,但不是必须持有。
func (c *Cond) Signal() {
	c.checker.check()
	runtime_notifyListNotifyOne(&c.notify)
}

在 Go 语言中,sync.Cond 类型的 Signal 方法会唤醒一个等待在条件变量上的 goroutine,但它并不会影响 goroutine 的调度优先级。这意味着使用 Signal 方法唤醒的 goroutine 和其他正常运行的 goroutine 之间不会有优先级上的差别。
此外,当调用 Signal 方法唤醒一个 goroutine 时,并不保证该 goroutine 会立即获取到条件变量关联的互斥锁。其他 goroutine 可能在此时尝试锁定互斥锁,并且它们可能会在被唤醒的 goroutine 之前获取到锁。因此,被唤醒的 goroutine 可能需要等待其他 goroutine 释放锁之后才能继续执行。
这种情况可能会导致一些竞态条件的发生,因为被唤醒的 goroutine 并不会立即执行,而是需要等待互斥锁。其他 goroutine 可能会在此期间修改共享状态,从而影响被唤醒的 goroutine 的行为。
因此,在使用 sync.Cond 类型时,需要特别注意在调用 Signal 方法后被唤醒的 goroutine 和其他竞争锁的 goroutine 之间可能存在的竞态条件。正确的处理方式应该是在唤醒的 goroutine 中重新检查条件并获取互斥锁,以确保在正确的条件下执行后续逻辑。

2.2.4 Broadcast广播

Broadcast会唤醒所有在 c 上等待的 goroutine。

  • 允许但不要求调用者在调用过程中持有 c.L。
func (c *Cond) Broadcast() {
	c.checker.check()
	runtime_notifyListNotifyAll(&c.notify)
}

2.3 示例

2.3.1 实现生产者-消费者模式

可以用Cond来实现生产者、消费者模式。(实际不会这么写,此处只是为了演示Cond的用法)

var (
	mux   = sync.Mutex{} // 互斥锁
	cond  = sync.NewCond(&mux) // 条件变量
	queue []int // 消息队列
)

// producer 生产者
func producer(count int) {
	for i := 0; i < count; i++ {
		mux.Lock() // 获取锁
		queue = append(queue, i+1) // 生产消息
		fmt.Printf("produce %d.\n", i+1)
		cond.Signal() // 通知消费者
		mux.Unlock() // 解锁
	}
}

// consumer 消费者
func consumer() {
	for {
		mux.Lock() // 获取锁
		for len(queue) == 0 { 
			cond.Wait() // 如果没有消息就等待
		}
		fmt.Printf("consume %d.\n", queue[0])
		queue = queue[1:]
		mux.Unlock() // 解锁
	}
}

func main() {
	go producer(10)
	go consumer()

	time.Sleep(time.Second)
}

2.3.2 多协程等待任务完成

可以使用 sync.Cond 让多个协程等待某个任务的完成。任务完成后通过 Broadcast 方法通知所有等待的协程。

var (
	mu    sync.Mutex
	cond  = sync.NewCond(&mu)
	done  bool
)

func worker(id int) {
	mu.Lock()
	for !done {
		cond.Wait()
	}
	fmt.Println("Worker", id, "completed")
	mu.Unlock()
}

func main() {
	for i := 0; i < 5; i++ {
		go worker(i)
	}

	// 模拟任务完成
	mu.Lock()
	done = true
	cond.Broadcast()
	mu.Unlock()

	// 主程序等待一段时间,让协程完成输出
	// 这里只是为了示例,实际情况可能需要更复杂的逻辑
	select {}
}

3 Locker锁接口

Locker是所有“锁”的接口,如Mutex、RWMutex等,这就不需要多说了。

A Locker represents an object that can be locked and unlocked.

type Locker interface {
	Lock()
	Unlock()
}

4 Mutex互斥锁

4.1 关于Mutex

Mutex是一个互斥锁。

  • Mutex的零值是一个未锁定的互斥锁。

特别注意:Mutex是不可重入的互斥锁!!!

type Mutex struct {
	state int32
	sema  uint32
}

4.2 Mutex的互斥公平性

Mutex有两种操作模式:

  • normal正常模式
    • 等待获取锁的 goroutine 会按照 FIFO(先进先出)的顺序排队。
    • 当一个被唤醒的等待者尝试获取锁时,它并不拥有锁,而是与新到达的 goroutine 竞争锁的所有权。
    • 新到达的 goroutine 有优势,因为它们已经在 CPU 上运行,并且可能存在大量新到达的 goroutine,因此被唤醒的等待者很可能会失败并重新排队在等待队列的最前面。
    • 如果一个等待者连续尝试获取锁超过1毫秒,那么锁会切换到饥饿模式。
  • starvation饥饿模式
    • 锁的所有权会直接从解锁的 goroutine 转移到等待队列最前面的等待者。
    • 新到达的 goroutine 不会尝试获取锁,即使锁看起来是解锁的,也不会自旋等待,而是排队在等待队列的尾部。
    • 如果一个等待者成功获取锁并发现自己是等待队列中的最后一个等待者,或者等待时间少于1毫秒,那么锁会切换回正常操作模式。

正常模式的性能要优于饥饿模式,因为在正常模式下,一个 goroutine 可以连续多次获取锁,即使存在被阻塞的等待者。饥饿模式的作用是防止尾部延迟的极端情况。
Go 标准库中的 sync.Mutex 实现了这样的模式切换机制,以兼顾性能和避免饥饿情况。这种设计可以确保在大多数情况下,正常模式下的性能表现良好,同时避免极端情况下的饥饿问题。

4.3 结构体方法

4.3.1 Lock获取锁

Lock方法用于获取互斥锁,如果互斥锁已经被占用,调用者将被阻塞,直到互斥锁可用。

func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

4.3.2 TryLock尝试获取锁

TryLock方法会获取互斥锁,并返回是否获取互斥锁成功。

请注意,虽然 TryLock 的正确使用确实存在,但很少见,并且 TryLock 的使用通常表明互斥体的特定使用中存在更深层次的问题。

func (m *Mutex) TryLock() bool {
	old := m.state
	if old&(mutexLocked|mutexStarving) != 0 {
		return false
	}

	// There may be a goroutine waiting for the mutex, but we are
	// running now and can try to grab the mutex before that
	// goroutine wakes up.
	if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
		return false
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
	return true
}

4.3.3 释放锁

Unlock方法将释放互斥锁(解锁)。

  • 如果调用Unlock方法时,互斥锁并没有被锁定,将会产生允许时错误。
  • 锁定的互斥锁不与特定的 goroutine 关联。允许一个 Goroutine 锁定一个 Mutex,然后安排另一个 Goroutine 解锁它。
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// Fast path: drop lock bit.
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// Outlined slow path to allow inlining the fast path.
		// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
		m.unlockSlow(new)
	}
}

4.4 示例

在Cond条件变量章节的示例中,就有mutex的使用,请参考Cond条件变量章节。

5 RWMutex读写锁

5.1 关于RWMutex

RWMutex是一个读写互斥锁,该锁可以被任意数量的读锁或者单个写锁持有。

  • RWMutex的零值是一个未锁的互斥锁。
  • 当由一个或多个goroutine持有一个RWMutex的读锁时,任意goroutine调用Lock方法,都会导致后续调用RLock方法的调用者被阻塞,直到写入者获取并释放锁,以确保锁最终可供写入者使用。

需要注意的是,sync.RWMutex 不支持递归的读取锁定,即同一个 goroutine 在持有读取锁的情况下再次尝试获取读取锁会导致死锁。这是为了避免潜在的死锁情况,强制要求编写代码时注意锁的持有情况。【我在实际测试过程中,发现同一个线程是可以重复获取读锁的。但是如果在递归中获取读锁,而又有其他协程获取写锁,可能会导致死锁。】
源码注释的原文为:
// If any goroutine calls Lock while the lock is already held by
// one or more readers, concurrent calls to RLock will block until
// the writer has acquired (and released) the lock, to ensure that
// the lock eventually becomes available to the writer.
// Note that this prohibits recursive read-locking.

type RWMutex struct {
	w           Mutex        // held if there are pending writers
	writerSem   uint32       // semaphore for writers to wait for completing readers
	readerSem   uint32       // semaphore for readers to wait for completing writers
	readerCount atomic.Int32 // number of pending readers
	readerWait  atomic.Int32 // number of departing readers
}

5.2 结构体方法

5.2.1 RLock获取读锁

RLock 锁定 rw 以进行读取。它不应该用于递归读锁定;阻塞的 Lock 调用会阻止新的读取者获取锁。

  • 如果当前有goroutine持有写锁,则阻塞。
  • 如果当前有goroutine持有读锁:
    • 如果有goroutine在等待写锁,则阻塞。
    • 如果无goroutine在等待写锁,则持有读锁。
func (rw *RWMutex) RLock() {
	...
}

5.2.2 TryRLock尝试获取读锁

TryRLock尝试获取读锁并返回是否获取成功。

// Note that while correct uses of TryRLock do exist, they are rare,
// and use of TryRLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (rw *RWMutex) TryRLock() bool {
	...
}

5.2.3 RUnlock 释放读锁

RUnlock释放单个读锁,对剩余的读者不影响。

  • 如果调用时,没有goroutine占有读锁,将会导致一个运行时错误。
func (rw *RWMutex) RUnlock() {
	...
}

5.2.4 RLocker获取读锁器

RLocker 返回一个 Locker 接口,通过调用 rw.RLock 和 rw.RUnlock 实现 Lock 和 Unlock 方法。

func (rw *RWMutex) RLocker() Locker {
	return (*rlocker)(rw)
}

type rlocker RWMutex

func (r *rlocker) Lock()   { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }

5.2.5 Lock获取写锁

Lock 锁定 rw 以进行写入。如果该锁已被锁定以进行读取或写入,则 Lock 会阻塞,直到该锁可用为止。

func (rw *RWMutex) Lock() {
...
}

5.2.6 TryLock尝试获取写锁

TryLock 尝试锁定 rw 进行写入并报告是否成功。

// Note that while correct uses of TryLock do exist, they are rare,
// and use of TryLock is often a sign of a deeper problem
// in a particular use of mutexes.
func (rw *RWMutex) TryLock() bool {
...
}

5.2.7 Unlock释放写锁

Unlock 释放写锁。

  • 如果 rw 被持有写锁,则会出现运行时错误。
  • 与互斥体一样,锁定的 RWMutex 不与特定的 goroutine 关联。一个 goroutine 可以 RLock(Lock)一个 RWMutex,然后安排另一个 goroutine 对其进行 RUnlock(Unlock)
func (rw *RWMutex) Unlock() {
	...
}

5.3 示例

5.3.1 并发读取,独占写入

var (
	rwMux = sync.RWMutex{}
	data  = make(map[int]int)
)

func read(key int) {
	rwMux.RLock()
	defer rwMux.RUnlock()
	fmt.Printf("read %d -> %d\n", key, data[key])
}

func write(key, value int) {
	rwMux.Lock()
	defer rwMux.Unlock()
	data[key] = value
	fmt.Printf("write %d -> %d\n", key, value)
}

6 Once单次执行

6.1 关于Once

sync.Once 可以确保在程序运行过程中某个操作只会执行一次,即使被多个 goroutine 同时调用。一旦操作执行完成,后续的调用会立即返回而不再执行。

type Once struct {
	done atomic.Uint32 // 指示动作是否被执行过
	m    Mutex // 互斥锁
}

6.2 结构体方法

6.2.1 Do执行

对于一个Once,只有在第一次调用Once的Do方法时,才会执行传入的方法。

  • 对于一个Once对象,如果调用多次Do方法(不论是否在同一个协程中调用),都只会在第一次调用时,才会执行传入的方法。即使每次传入的参数不同。
  • 每个函数的执行,都需要一个新的Once实例。
func (o *Once) Do(f func()) {
	if o.done.Load() == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

特别注意:

  • 因为在对 f 的一次调用返回之前,对 Do 的调用不会返回,所以如果 f 导致 Do 被调用,就会出现死锁。在下面的示例中,外层传入的方法中,又一次调用了once.Do方法,这将导致死锁!
func main() {
	once := sync.Once{}

	once.Do(func() {
		once.Do(func() { 
			fmt.Println("HELLO WORLD")
		})
	})
}
  • 如果f发生恐慌,Do认为它已经返回;以后对 Do 的调用将返回而不调用 f。

6.3 Once与函数字面量

Do 方法用于需要确保只运行一次的初始化操作。由于被执行的函数 f 是无参数的,因此有时候可能需要使用函数字面量(function literal)来捕获函数的参数,以便在 Do 方法中调用。

函数字面量是匿名函数的一种形式,可以在声明时直接定义函数体。通过函数字面量,我们可以将需要传递给函数的参数捕获到闭包中,然后在 Do 方法中调用这个函数。

func main() {
	var once sync.Once

	// 准备一个带参数的函数
	initFunc := func(message string) {
		fmt.Println("Initializing with message:", message)
	}

	// 使用函数字面量来捕获参数并传递给初始化函数
	message := "Hello, World!"
	once.Do(func() {
		initFunc(message)
	})

	// 再次调用 Do 方法,函数将不会再次执行
	once.Do(func() {
		fmt.Println("This won't be executed again")
	})
}

6.4 Once相关方法

6.4.1 OnceFunc

OnceFunc 返回一个仅调用 f 一次的函数。返回的函数可以被并发调用。如果 f 发生恐慌,则返回的函数将在每次调用时以相同的值发生恐慌。

func OnceFunc(f func()) func() {
	...
}
func main() {
	onceFunc := sync.OnceFunc(func() {
		fmt.Println("Hello World")
	})
	go onceFunc() 
	go onceFunc()
	go onceFunc()
	go onceFunc()
	time.Sleep(time.Second)
}
// 只会打印一次Hello World

6.4.2 OnceValue

OnceValue 返回一个仅调用 f 一次的函数,并返回 f 返回的值。返回的函数可以被并发调用。如果 f 发生恐慌,则返回的函数将在每次调用时以相同的值发生恐慌。

  • 如果第一次执行没有panic,后续调用会直接返回第一次执行的结果。
func OnceValue[T any](f func() T) func() T {
	···
}
func main() {
	onceValue := sync.OnceValue[string](func() string {
		return time.Now().Format("20060102150405")
	})

	print := func() {
		fmt.Println(onceValue())
	}

	go print()
	go print()
	go print()
	time.Sleep(time.Second)
}
// 程序将打印三次同一个结果

6.4.3 OnceValues

OnceValues 返回一个仅调用 f 一次的函数,并返回 f 返回的值。返回的函数可以被并发调用。如果 f 发生恐慌,则返回的函数将在每次调用时以相同的值发生恐慌。

  • 如果第一次执行没有panic,后续调用会直接返回第一次执行的结果。
func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) {
	...
}

该方法与OnceValue方法差不多,只是返回值的数量为两个而已。

7 WaitGroup等待集

7.1 关于WaitGroup

WaitGroup用于等待一组goroutine执行结束。

要等待的主线程,通过调用Add方法来设定需要等待的goroutine数量。每个被等待的gorotine在执行完成时调用Done方法。等待的主线程将阻塞,直到所有被等待的goroutine执行完成。

type WaitGroup struct {
	noCopy noCopy

	state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
	sema  uint32
}

7.2 结构体方法

7.2.1 Add添加等待数量

Add方法向WaitGroup的计数器添加delta个等待数量。

  • delta可能为负数。
  • 如果WaitGroup的计数器变成0,所有在Wait该WaitGroup都将被唤醒。
  • 如果WaitGroup的计数器变成了负数,Add方法将panic。
func (wg *WaitGroup) Add(delta int) {
...
}

当计数器为0时,Add一个正数的操作应该在Wait操作之前。反之,Wait操作将不会阻塞。
当计数器大于0时,Add一个正数或负数可能发生在任何时候。
通常,这意味着对 Add 的调用应该在创建 goroutine 或其他要等待的事件的语句之前执行。如果重复使用 WaitGroup 来等待多个独立的事件集,则必须在所有先前的 Wait 调用返回后发生新的 Add 调用。

7.2.2 Done减少一个等待数量

被等待的goroutine需要在执行完成后,调用Done方法来使WaitGroup的计数器减一。从源码可以看出,Done实际上是调用了Add方法。

func (wg *WaitGroup) Done() {
	wg.Add(-1)
}

7.2.3 Wait等待

调用Wait方法的goroutine将被阻塞,直到WaitGroup的计数器变成0。

func (wg *WaitGroup) Wait() {
...
}

7.3 示例

7.3.1 官方示例

// This example fetches several URLs concurrently,
// using a WaitGroup to block until all the fetches are complete.
func ExampleWaitGroup() {
	var wg sync.WaitGroup
	var urls = []string{
		"http://www.golang.org/",
		"http://www.google.com/",
		"http://www.example.com/",
	}
	for _, url := range urls {
		// Increment the WaitGroup counter.
		wg.Add(1)
		// Launch a goroutine to fetch the URL.
		go func(url string) {
			// Decrement the counter when the goroutine completes.
			defer wg.Done()
			// Fetch the URL.
			http.Get(url)
		}(url)
	}
	// Wait for all HTTP fetches to complete.
	wg.Wait()
}

8 Pool池

8.1 关于Pool

Pool是一组可以单独保存和检索的临时对象的集合。

  • 任何存储在 sync.Pool 中的项都可能在任何时候被自动移除,而没有任何通知。如果在移除操作发生时,sync.Pool 是存储项的唯一引用,那么这个项可能会被释放或回收。

    在使用 sync.Pool 时,建议遵循以下最佳实践:

    • 不依赖于池中对象的存在性,尽量避免在对象被移除后继续使用。
    • 针对临时对象的重用和性能优化,而不是长期存储。
    • 在池中存储的对象应该是无状态或者可重置状态的,以便于重复使用。
  • 多个 goroutine 同时使用 Pool 是安全的。

  • sync.Pool 的作用是缓存已分配但未使用的项,以供以后重复使用,从而减轻垃圾回收器的压力。换句话说,它可以轻松构建高效、线程安全的空闲列表(free list)。但并不适用于所有类型的空闲列表。

  • 池的其中一个适当用途是用来管理一组临时项,这些项在并发独立的客户端之间静默共享,并且有可能被重复使用。sync.Pool 提供了一种方式来在许多客户端之间分摊分配开销。

  • 在 fmt 包中,sync.Pool 被用来维护一个动态大小的临时输出缓冲区存储。这个存储会根据负载情况进行扩展(当有很多 goroutine 在活跃地进行打印操作时),并在空闲时收缩。

  • 对于生命周期短暂的对象,维护一个空闲列表可能会增加额外的开销,不适合使用 sync.Pool。
    在这种情况下,最好让对象自己实现空闲列表的管理,以更好地控制资源的分配和释放。

type Pool struct {
	noCopy noCopy

	local     unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
	localSize uintptr        // size of the local array

	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array

	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
	New func() any
}

8.2 结构体方法

8.2.1 New生成对象

New用于指定一个函数,用于在 Get 方法返回 nil 时生成一个值。同时指出,New 方法不应该在并发调用 Get 方法的情况下被同时修改。

在使用 sync.Pool 时,可以通过 New 方法指定一个函数,在 Get 方法无法返回值时,用于生成一个新的值。这样可以避免返回 nil,并且可以根据需要动态地生成新的值。需要注意的是,在调用 Get 方法的同时,不应该并发地修改 New 方法,以避免出现竞争条件和不确定的行为。

New 方法为 sync.Pool 提供了一种灵活的方式来生成值,从而确保在需要时能够获取到有效的值。

type Pool struct {
	...

	// New optionally specifies a function to generate
	// a value when Get would otherwise return nil.
	// It may not be changed concurrently with calls to Get.
	New func() any
}

8.2.2 Put

Put方法用于向池中添加以一个临时对象x。

func (p *Pool) Put(x any) {
	...
}

8.2.3 Get

Get 方法用于从池中获取一个项,并将其移除。需要注意的是,Get 方法可能会忽略池中的内容并返回一个新的项。此外,调用方不应该假设放入池中的值与 Get 返回的值之间存在任何关系,因为 Get 方法的行为是无法预测的。

func (p *Pool) Get() any {
	...
}

8.3 示例

8.3.1 官方示例

var bufPool = sync.Pool{
	New: func() any {
		// The Pool's New function should generally only return pointer
		// types, since a pointer can be put into the return interface
		// value without an allocation:
		return new(bytes.Buffer)
	},
}

// timeNow is a fake version of time.Now for tests.
func timeNow() time.Time {
	return time.Unix(1136214245, 0)
}

func Log(w io.Writer, key, val string) {
	b := bufPool.Get().(*bytes.Buffer)
	b.Reset()
	// Replace this with time.Now() in a real logger.
	b.WriteString(timeNow().UTC().Format(time.RFC3339))
	b.WriteByte(' ')
	b.WriteString(key)
	b.WriteByte('=')
	b.WriteString(val)
	w.Write(b.Bytes())
	bufPool.Put(b)
}

func ExamplePool() {
	Log(os.Stdout, "path", "/search?q=flowers")
	// Output: 2006-01-02T15:04:05Z path=/search?q=flowers
}

9 Map 并发安全的map

9.1 关于Map

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

  • 虽然 sync.Map 提供了一个方便且高效的线程安全映射结构,但在某些情况下,使用普通的 Go map 并结合适当的锁定机制可能更为合适。这样可以更好地控制并发访问,确保类型安全性,并且更容易维护与映射内容相关的其他约束条件。
  • Map的零值是一个可以随时使用的空Map
  • 写操作会在任何观察到该写操作效果的读操作之前“同步”。这意味着当一个写操作发生后,任何观察到这个写操作效果的读操作都会在该写操作之后执行,确保了写操作的效果被读操作正确地观察到。
  • Map 类型针对两种常见用例进行了优化:
    1. 当给定键的条目仅写入一次但读取多次时,如在只会增长的缓存中,
    2. 当多个 goroutine 读取、写入和读取时覆盖不相交的键集的条目。

在这两种情况下,与与单独的 Mutex 或 RWMutex 配对的 Go Map 相比,使用 Map 可以显着减少锁争用。

type Map struct {
	mu Mutex

	// read contains the portion of the map's contents that are safe for
	// concurrent access (with or without mu held).
	//
	// The read field itself is always safe to load, but must only be stored with
	// mu held.
	//
	// Entries stored in read may be updated concurrently without mu, but updating
	// a previously-expunged entry requires that the entry be copied to the dirty
	// map and unexpunged with mu held.
	read atomic.Pointer[readOnly]

	// dirty contains the portion of the map's contents that require mu to be
	// held. To ensure that the dirty map can be promoted to the read map quickly,
	// it also includes all of the non-expunged entries in the read map.
	//
	// Expunged entries are not stored in the dirty map. An expunged entry in the
	// clean map must be unexpunged and added to the dirty map before a new value
	// can be stored to it.
	//
	// If the dirty map is nil, the next write to the map will initialize it by
	// making a shallow copy of the clean map, omitting stale entries.
	dirty map[any]*entry

	// misses counts the number of loads since the read map was last updated that
	// needed to lock mu to determine whether the key was present.
	//
	// Once enough misses have occurred to cover the cost of copying the dirty
	// map, the dirty map will be promoted to the read map (in the unamended
	// state) and the next store to the map will make a new dirty copy.
	misses int
}

9.2 结构体方法

9.2.1 Load

Load方法返回存储在Map中的指定Key的值。

  • 如果Key在Map中不存在,返回nil
  • 第二个返回值ok,用于指定Map是否存储了该Key
func (m *Map) Load(key any) (value any, ok bool) {
...
}

9.2.2 Store

Store方法项map中设置指定的键值对

  • 实际调用的时Swap方法
func (m *Map) Store(key, value any) {
	_, _ = m.Swap(key, value)
}

9.2.3 LoadOrStore

LoadOrStore方法用于检查指定Key是否存在并返回Key对应的值。

  • 如果指定Key在Map中存在,就返回原来的值。
  • 如果指定Key在Map中不存在,就将传入的键值对保存到Map中,并返回传入的值。
  • 第二个返回值用于标识原来是否有值(即是否load了原来的值)
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
...
}

9.2.4 LoadAndDelete

LoadAndDelete方法用于加载并删除指定的Key。

  • 如果Key在Map中存在,就返回原来的值,并将该键值对从Map中删除。
  • 如果Key在Map中不存在,就返回nil。
  • 第二个返回值用于标识原来是否有值。
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
...
}

9.2.5 Delete

删除Map中指定的Key。

  • 实际上调用的是LoadAndDelete方法。
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}

9.2.6 Swap

Swap方法将指定的键值对存储到Map中,并返回原先存储的值。

  • 如果原先无值,就返回nil。
  • 第二个返回值用于标识原先是否有值。
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
...
}

9.2.7 CompareAndSwap

CompareAndSwap方法比较Map中指定Key的值是否等于old,如果等于old就设置新的值。

  • old的值必须是可比较的类型。
  • 返回值用于标识是否执行了Swap。
func (m *Map) CompareAndSwap(key, old, new any) bool {
...
}

9.2.8 CompareAndDelete

CompareAndDelete方法比较Map中指定Key的值是否等于old,如果等于old就删除指定的键值对。

  • old必须是可以比较的类型。
  • 返回值用于标识是否执行了删除操作。
  • 如果当前Map中没有指定的Key,返回始终返回false,即使传入的old为nil。
func (m *Map) CompareAndDelete(key, old any) (deleted bool) {
...
}

9.2.9 Range

Range 为映射中存在的每个键和值依次调用 f 。如果 f 返回 false,则 range 停止迭代。

Range 不一定对应于 Map 内容的任何一致快照:不会多次访问任何键,但如果同时存储或删除任何键的值(包括通过 f),Range 可能会反映该键的任何映射Range 调用期间的任意点。 Range 不会阻塞接收器上的其他方法;甚至 f 本身也可以调用 m 上的任何方法。

func (m *Map) Range(f func(key, value any) bool) {
...
}

9.3 示例

9.3.1 并发读取和写入

func main() {
	var m sync.Map
	var wg sync.WaitGroup
	numRoutines := 5

	// 并发写入
	for i := 0; i < numRoutines; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			m.Store(i, fmt.Sprintf("value%d", i))
		}(i)
	}

	// 等待所有写入操作完成
	wg.Wait()

	// 并发读取
	for i := 0; i < numRoutines; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			if value, ok := m.Load(i); ok {
				fmt.Printf("Key: %v, Value: %v\n", i, value)
			}
		}(i)
	}

	// 等待所有读取操作完成
	wg.Wait()
}

9.3.2 并发删除

func main() {
	var m sync.Map
	var wg sync.WaitGroup
	numKeys := 5

	// 初始化 sync.Map
	for i := 0; i < numKeys; i++ {
		m.Store(i, fmt.Sprintf("value%d", i))
	}

	// 并发删除
	for i := 0; i < numKeys; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			m.Delete(i)
			fmt.Printf("Deleted key: %v\n", i)
		}(i)
	}

	// 等待所有删除操作完成
	wg.Wait()
}
  • 32
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值