Go 共享内存并发:从入门到精通,这一篇就够了!

177 阅读2分钟

什么是共享内存并发?

在传统的并发模型中,多个线程或 Goroutine 共享同一块内存空间。这意味着它们可以直接访问和修改相同的数据,而无需进行显式的数据传递。这种方式可以提高效率,但也带来了数据竞争和同步问题。

Go 语言中的共享内存并发

在 Go 语言中,Goroutine 是轻量级的并发执行单元。多个 Goroutine 可以并发地访问和修改共享内存。

示例代码:

package main

import (
	"fmt"
	"sync"
)

var counter int = 0
var wg sync.WaitGroup //`sync.WaitGroup`是一个用于等待一组goroutine完成的同步机制

func increment() {
	defer wg.Done() //在函数返回前调用Done()
	for i := 0; i < 5000; i++ {
		counter++ // 潜在的数据竞争
	}
}

func main() {
	wg.Add(2) //告诉`WaitGroup`你将要启动2个goroutine
	go increment()
	go increment()
	wg.Wait() //在所有goroutine启动后,使用`Wait()`方法阻塞当前goroutine,直到所有通过`Add()`添加的任务都通过调用`Done()`完成
	fmt.Println("Counter:", counter)
}

问题:数据竞争

上面的代码看似简单,但存在严重的数据竞争问题。由于多个 Goroutine 同时修改 counter 变量,导致最终结果可能不正确。

如何解决数据竞争?

Go 语言提供了多种机制来解决共享内存并发中的数据竞争问题:

  • 互斥锁(Mutex): 互斥锁是最常用的同步机制。它可以确保在同一时刻只有一个 Goroutine 可以访问共享资源。

    package main
    
    import (
    	"fmt"
    	"runtime"
    	"sync"
    )
    
    var counter int = 0
    var wg sync.WaitGroup 
    var mu sync.Mutex // 互斥锁
    
    func increment() {
    	defer wg.Done()  
    	for i := 0; i < 1000; i++ {
    		mu.Lock()   // 加锁
    		counter++
    		mu.Unlock() // 解锁
    	}
    }
    
    func main() {
    	wg.Add(2) 
    	go increment()
    	go increment()
    	wg.Wait()
    	fmt.Println("Counter:", counter)
    }
    
  • 读写锁(RWMutex): 读写锁允许多个 Goroutine 同时读取共享资源,但只允许一个 Goroutine 写入共享资源。这在读多写少的场景下可以提高性能。

    package main
    
    import (
    	"fmt"
    	"runtime"
    	"sync"
    )
    
    var counter int = 0
    var wg sync.WaitGroup
    var rwmu sync.RWMutex // 读写锁
    
    func readCounter() {
    	defer wg.Done()
    	rwmu.RLock()   // 读锁
    	fmt.Println("Counter:", counter)
    	rwmu.RUnlock() // 释放读锁
    }
    
    func increment() {
    	defer wg.Done()
    	rwmu.Lock()   // 写锁
    	counter++
    	rwmu.Unlock() // 释放写锁
    }
    
    func main() {
    	wg.Add(3)
    	go readCounter()
    	go increment()
    	go readCounter()
    	wg.Wait()
    	fmt.Println("Final Counter:", counter)
    }
    
  • 原子操作(Atomic Operations): 原子操作是不可分割的操作,可以保证在多线程环境下对共享变量的访问是安全的。

    package main
    
    import (
    	"fmt"
    	"runtime"
    	"sync"
    	"sync/atomic"
    )
    
    var counter int64 = 0
    var wg sync.WaitGroup
    
    func increment() {
    	defer wg.Done()
    	for i := 0; i < 5000; i++ {
    		atomic.AddInt64(&counter, 1) // 原子操作
    	}
    }
    
    func main() {
    	wg.Add(2)
    	go increment()
    	go increment()
    	wg.Wait()
    	fmt.Println("Counter:", counter)
    }
    

总结

共享内存并发是 Go 语言中实现高性能并发应用的重要技术。通过理解其原理、掌握其用法,并避免常见的陷阱,你可以编写出高效、可靠的并发程序。

感谢阅读!如果你觉得这篇文章对你有帮助,请分享给你的朋友们,让更多的人一起学习Go语言!