MST

星途 面试题库

面试题:Go语言指针slice在并发场景下的高级应用

在并发编程中,使用Go语言的指针slice,如何确保数据的一致性和避免竞态条件?请描述在多个goroutine同时读写指针slice时,常用的同步机制和最佳实践,并给出示例代码。
49.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试
  1. 常用同步机制
    • 互斥锁(sync.Mutex
      • 原理:互斥锁用于保护共享资源,在同一时间只允许一个goroutine访问共享资源。当一个goroutine获取了互斥锁,其他goroutine必须等待锁被释放才能获取并访问资源。
      • 使用场景:适用于读写操作频繁且对性能要求不是极高的场景,因为加锁和解锁操作会有一定的性能开销。
    • 读写锁(sync.RWMutex
      • 原理:读写锁允许多个goroutine同时进行读操作,但只允许一个goroutine进行写操作。读操作之间不会互斥,而写操作会排斥读操作和其他写操作。
      • 使用场景:适用于读操作远远多于写操作的场景,这样可以提高并发性能,减少读操作的等待时间。
    • 通道(chan
      • 原理:通过通道进行数据传递,所有对指针slice的操作都通过通道来进行。goroutine将操作(如添加、修改、读取)封装成消息发送到通道,由一个专门的goroutine从通道接收消息并执行操作,这样就避免了多个goroutine直接并发访问指针slice。
      • 使用场景:适用于需要对指针slice进行复杂操作且希望以消息驱动方式进行处理的场景。
  2. 最佳实践
    • 尽量减少共享状态,能不共享数据就不共享,将数据分散到各个goroutine中,避免竞态条件的产生。
    • 明确读写操作频率,根据读写频率选择合适的同步机制,如读多写少用读写锁,读写均衡或写多读少用互斥锁。
    • 对指针slice的操作进行封装,将同步逻辑放在封装的函数内部,使外部调用者无需关心同步细节,提高代码的可维护性和安全性。
  3. 示例代码
    • 使用互斥锁
package main

import (
    "fmt"
    "sync"
)

var (
    mutex    sync.Mutex
    pointerSlice []*int
)

func addElement(num int) {
    mutex.Lock()
    defer mutex.Unlock()
    newNum := num
    pointerSlice = append(pointerSlice, &newNum)
}

func readElement(index int) *int {
    mutex.Lock()
    defer mutex.Unlock()
    if index < len(pointerSlice) {
        return pointerSlice[index]
    }
    return nil
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            addElement(n)
        }(i)
    }
    go func() {
        wg.Wait()
        for i := 0; i < len(pointerSlice); i++ {
            fmt.Println(*readElement(i))
        }
    }()
    select {}
}
  • 使用读写锁
package main

import (
    "fmt"
    "sync"
)

var (
    rwMutex  sync.RWMutex
    pointerSlice []*int
)

func addElement(num int) {
    rwMutex.Lock()
    defer rwMutex.Unlock()
    newNum := num
    pointerSlice = append(pointerSlice, &newNum)
}

func readElement(index int) *int {
    rwMutex.RLock()
    defer rwMutex.RUnlock()
    if index < len(pointerSlice) {
        return pointerSlice[index]
    }
    return nil
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            addElement(n)
        }(i)
    }
    go func() {
        wg.Wait()
        for i := 0; i < len(pointerSlice); i++ {
            fmt.Println(*readElement(i))
        }
    }()
    select {}
}
  • 使用通道
package main

import (
    "fmt"
    "sync"
)

type sliceOp struct {
    opType string
    num    int
    index  int
    result chan *int
}

var pointerSlice []*int

func sliceOperator(opChan chan sliceOp) {
    for op := range opChan {
        switch op.opType {
        case "add":
            newNum := op.num
            pointerSlice = append(pointerSlice, &newNum)
        case "read":
            if op.index < len(pointerSlice) {
                op.result <- pointerSlice[op.index]
            } else {
                op.result <- nil
            }
        }
    }
}

func main() {
    var wg sync.WaitGroup
    opChan := make(chan sliceOp)
    go sliceOperator(opChan)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            resultChan := make(chan *int)
            opChan <- sliceOp{opType: "add", num: n}
            close(resultChan)
        }(i)
    }
    go func() {
        wg.Wait()
        for i := 0; i < len(pointerSlice); i++ {
            resultChan := make(chan *int)
            opChan <- sliceOp{opType: "read", index: i, result: resultChan}
            fmt.Println(*<-resultChan)
            close(resultChan)
        }
        close(opChan)
    }()
    select {}
}