面试题答案
一键面试可能出现的数据竞争及并发问题
- 数据覆盖:多个线程同时对切片进行写操作,可能导致其中一个线程的数据被另一个线程覆盖。例如:
package main
import (
"fmt"
"sync"
)
var s []int
var wg sync.WaitGroup
func writeSlice(i int) {
defer wg.Done()
s = append(s, i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go writeSlice(i)
}
wg.Wait()
fmt.Println(s)
}
在上述代码中,多个goroutine
同时向slice
中append
数据,由于append
操作并非原子性,可能导致数据覆盖或不一致的结果。
- 读取未初始化数据:如果一个线程读取切片数据,而另一个线程正在对切片进行初始化或扩容操作,可能读到未初始化的数据。例如:
package main
import (
"fmt"
"sync"
)
var s []int
var wg sync.WaitGroup
func initSlice() {
defer wg.Done()
s = make([]int, 10)
for i := 0; i < 10; i++ {
s[i] = i
}
}
func readSlice() {
defer wg.Done()
if len(s) > 0 {
fmt.Println(s[0])
}
}
func main() {
wg.Add(2)
go initSlice()
go readSlice()
wg.Wait()
}
在这个例子中,readSlice
可能在initSlice
还未完全初始化slice
时就尝试读取数据,导致读取到未初始化的值。
避免问题的方法及底层原理分析
- 使用互斥锁(Mutex):
- 原理:互斥锁通过加锁和解锁机制,保证同一时间只有一个
goroutine
可以访问共享资源(这里是切片)。 - 示例:
- 原理:互斥锁通过加锁和解锁机制,保证同一时间只有一个
package main
import (
"fmt"
"sync"
)
var s []int
var mu sync.Mutex
var wg sync.WaitGroup
func writeSlice(i int) {
defer wg.Done()
mu.Lock()
s = append(s, i)
mu.Unlock()
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go writeSlice(i)
}
wg.Wait()
fmt.Println(s)
}
- 使用读写锁(RWMutex):
- 原理:当读操作远多于写操作时,读写锁可以提高并发性能。读操作可以同时进行,但写操作需要独占锁,以防止数据竞争。
- 示例:
package main
import (
"fmt"
"sync"
)
var s []int
var mu sync.RWMutex
var wg sync.WaitGroup
func writeSlice(i int) {
defer wg.Done()
mu.Lock()
s = append(s, i)
mu.Unlock()
}
func readSlice() {
defer wg.Done()
mu.RLock()
if len(s) > 0 {
fmt.Println(s[0])
}
mu.RUnlock()
}
func main() {
for i := 0; i < 5; i++ {
wg.Add(1)
go writeSlice(i)
}
for i := 0; i < 5; i++ {
wg.Add(1)
go readSlice()
}
wg.Wait()
}
- 使用通道(Channel):
- 原理:通道是Go语言中用于在
goroutine
之间进行通信的机制,它可以保证数据的顺序性和安全性。通过通道传递数据,避免了对共享切片的直接并发访问。 - 示例:
- 原理:通道是Go语言中用于在
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int)
var s []int
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
ch <- j
}(i)
}
go func() {
for i := range ch {
s = append(s, i)
}
}()
go func() {
wg.Wait()
close(ch)
}()
wg.Wait()
fmt.Println(s)
}
在这个例子中,goroutine
通过通道ch
将数据发送出来,然后由一个goroutine
负责从通道中接收数据并添加到切片 s
中,避免了直接并发访问切片带来的数据竞争问题。
切片在Go语言中底层是一个结构体,包含指向数据的指针、长度和容量。并发操作时,如果不进行同步控制,很容易因为多个goroutine
同时修改指针、长度或容量,导致数据不一致或程序崩溃。上述同步机制就是通过控制对切片底层结构体的访问顺序,来避免这些问题的发生。