面试题答案
一键面试Go 并发编程中的内存一致性模型
Go 的内存一致性模型基于 happens - before 关系。如果一个事件 A 在事件 B 之前发生(A happens - before B),那么 A 的影响对 B 可见。例如,对共享变量的写操作先于读操作,那么读操作能看到写操作的结果。
原子操作保证内存一致性的示例
原子操作通过底层硬件指令保证操作的原子性和内存一致性。例如,使用 sync/atomic
包中的 AddInt64
函数:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var counter int64
func increment(wg *sync.WaitGroup) {
defer wg.Done()
atomic.AddInt64(&counter, 1)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter value:", atomic.LoadInt64(&counter))
}
在这个例子中,atomic.AddInt64
保证了对 counter
的递增操作是原子的,多个协程同时操作不会导致数据竞争,从而保证了内存一致性。
原子操作适用场景
- 简单数据类型和高频操作场景:当操作简单数据类型(如整数、指针)且操作频率非常高时,原子操作更合适。因为互斥锁的加锁和解锁操作有一定开销,而原子操作直接在硬件层面实现,开销较小。例如,在一个高并发的计数器场景下,原子操作的性能优势明显。
- 细粒度同步需求:如果只需要对数据的某一部分进行同步,原子操作可以提供更细粒度的控制。比如,在一个包含多个字段的结构体中,只对其中一个整数字段进行并发安全的更新,使用原子操作就不需要锁住整个结构体。
复杂数据结构(链表)在多协程环境下的处理
- 综合运用原子操作和互斥锁:对于链表的头指针或尾指针等关键部分,可以使用原子操作来保证指针的更新是原子的,避免数据竞争。例如,使用
atomic.StorePointer
和atomic.LoadPointer
来操作链表头指针。
package main
import (
"fmt"
"sync"
"sync/atomic"
)
type Node struct {
value int
next *Node
}
var head atomic.Pointer[Node]
func addNode(value int) {
newNode := &Node{value: value}
for {
oldHead := head.Load()
newNode.next = oldHead
if head.CompareAndSwap(oldHead, newNode) {
break
}
}
}
func printList() {
current := head.Load()
for current != nil {
fmt.Printf("%d -> ", current.value)
current = current.next
}
fmt.Println("nil")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
addNode(val)
}(i)
}
wg.Wait()
printList()
}
- 结合读写锁:当链表有大量读操作和少量写操作时,可以使用读写锁(
sync.RWMutex
)。读操作时,多个协程可以同时获取读锁进行读取;写操作时,获取写锁,保证写操作的原子性和数据一致性。
package main
import (
"fmt"
"sync"
)
type Node struct {
value int
next *Node
}
var listHead *Node
var listMutex sync.RWMutex
func addNode(value int) {
newNode := &Node{value: value}
listMutex.Lock()
newNode.next = listHead
listHead = newNode
listMutex.Unlock()
}
func printList() {
listMutex.RLock()
current := listHead
for current != nil {
fmt.Printf("%d -> ", current.value)
current = current.next
}
fmt.Println("nil")
listMutex.RUnlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
addNode(val)
}(i)
}
wg.Wait()
printList()
}
- 使用通道:可以将对链表的操作封装成消息,通过通道发送给专门处理链表操作的协程。这样,所有对链表的操作都在一个协程中顺序执行,避免了多协程直接操作链表带来的数据竞争问题。
package main
import (
"fmt"
"sync"
)
type Node struct {
value int
next *Node
}
type ListOp struct {
op string
data int
}
func listProcessor(listChan chan ListOp) {
var head *Node
for op := range listChan {
switch op.op {
case "add":
newNode := &Node{value: op.data}
newNode.next = head
head = newNode
case "print":
current := head
for current != nil {
fmt.Printf("%d -> ", current.value)
current = current.next
}
fmt.Println("nil")
}
}
}
func main() {
var wg sync.WaitGroup
listChan := make(chan ListOp)
go listProcessor(listChan)
for i := 0; i < 5; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
listChan <- ListOp{op: "add", data: val}
}(i)
}
go func() {
wg.Wait()
listChan <- ListOp{op: "print"}
close(listChan)
}()
}
通过综合运用这些机制,可以在保证数据正确性的同时,提高多协程环境下复杂数据结构操作的性能。