面试题答案
一键面试不同底层类型的数据竞争特点
- 基本数据类型
- 特点:多个 goroutine 同时读写基本数据类型(如 int、float、bool 等)时,可能出现数据竞争。例如,一个 goroutine 读取某个 int 变量值,同时另一个 goroutine 对其进行修改,可能导致读取到不一致的数据。
- 示例:
package main
import (
"fmt"
"sync"
)
var num int
func write(wg *sync.WaitGroup) {
defer wg.Done()
num++
}
func read(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(num)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go write(&wg)
}
for i := 0; i < 10; i++ {
wg.Add(1)
go read(&wg)
}
wg.Wait()
}
在上述代码中,多个 goroutine 同时对 num
进行读写,可能会出现数据竞争,导致输出结果不稳定。
- 结构体
- 特点:当结构体包含可读写的字段,多个 goroutine 同时访问和修改这些字段时会产生数据竞争。即使结构体本身是不可变的,但如果其字段是可变的,也存在风险。
- 示例:
package main
import (
"fmt"
"sync"
)
type Person struct {
Name string
Age int
}
var p Person
func update(wg *sync.WaitGroup) {
defer wg.Done()
p.Age++
p.Name = "newName"
}
func printInfo(wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go update(&wg)
}
for i := 0; i < 10; i++ {
wg.Add(1)
go printInfo(&wg)
}
wg.Wait()
}
这里多个 goroutine 对 Person
结构体的字段进行读写,可能引发数据竞争。
- 指针
- 特点:指针指向的数据可能被多个 goroutine 通过指针进行读写操作,从而导致数据竞争。尤其是当指针指向可变的数据结构时,风险更高。
- 示例:
package main
import (
"fmt"
"sync"
)
var ptr *int
func modifyPtr(wg *sync.WaitGroup) {
defer wg.Done()
if ptr == nil {
num := 1
ptr = &num
} else {
*ptr++
}
}
func readPtr(wg *sync.WaitGroup) {
defer wg.Done()
if ptr != nil {
fmt.Println(*ptr)
}
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go modifyPtr(&wg)
}
for i := 0; i < 10; i++ {
wg.Add(1)
go readPtr(&wg)
}
wg.Wait()
}
在该示例中,多个 goroutine 通过指针 ptr
对其指向的数据进行读写,可能产生数据竞争。
避免数据竞争的策略
- 使用
sync.Mutex
- 原理:互斥锁(Mutex)用于保证同一时间只有一个 goroutine 可以访问共享资源。
- 示例:
package main
import (
"fmt"
"sync"
)
var num int
var mu sync.Mutex
func write(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
num++
mu.Unlock()
}
func read(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
fmt.Println(num)
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go write(&wg)
}
for i := 0; i < 10; i++ {
wg.Add(1)
go read(&wg)
}
wg.Wait()
}
在上述代码中,通过 mu.Lock()
和 mu.Unlock()
保护对 num
的读写操作,避免数据竞争。
- 使用
sync.RWMutex
- 原理:读写互斥锁(RWMutex)允许多个 goroutine 同时读共享资源,但只允许一个 goroutine 写。适用于读多写少的场景。
- 示例:
package main
import (
"fmt"
"sync"
)
var num int
var rwmu sync.RWMutex
func write(wg *sync.WaitGroup) {
defer wg.Done()
rwmu.Lock()
num++
rwmu.Unlock()
}
func read(wg *sync.WaitGroup) {
defer wg.Done()
rwmu.RLock()
fmt.Println(num)
rwmu.RUnlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go write(&wg)
}
for i := 0; i < 10; i++ {
wg.Add(1)
go read(&wg)
}
wg.Wait()
}
这里使用 rwmu.RLock()
进行读操作,rwmu.Lock()
进行写操作,在保证写操作原子性的同时,提高读操作的并发性能。
- 使用
sync.Map
- 原理:
sync.Map
是一个线程安全的键值对集合,内部实现了对并发读写的保护,无需用户手动加锁。 - 示例:
- 原理:
package main
import (
"fmt"
"sync"
)
var m sync.Map
func write(key string, value int, wg *sync.WaitGroup) {
defer wg.Done()
m.Store(key, value)
}
func read(key string, wg *sync.WaitGroup) {
defer wg.Done()
if val, ok := m.Load(key); ok {
fmt.Printf("Key: %s, Value: %d\n", key, val)
}
}
func main() {
var wg sync.WaitGroup
keys := []string{"k1", "k2", "k3"}
for i, key := range keys {
wg.Add(1)
go write(key, i, &wg)
}
for _, key := range keys {
wg.Add(1)
go read(key, &wg)
}
wg.Wait()
}
sync.Map
的 Store
和 Load
方法保证了并发操作的安全性。
- 使用通道(Channel)
- 原理:通过通道在 goroutine 之间传递数据,避免共享数据,从而从根本上避免数据竞争。
- 示例:
package main
import (
"fmt"
"sync"
)
func producer(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func consumer(ch chan int, wg *sync.WaitGroup) {
defer wg.Done()
for num := range ch {
fmt.Println(num)
}
}
func main() {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
go producer(ch, &wg)
wg.Add(1)
go consumer(ch, &wg)
wg.Wait()
}
在这个例子中,通过通道 ch
在 producer
和 consumer
goroutine 之间传递数据,避免了共享变量带来的数据竞争。