MST

星途 面试题库

面试题:Go调度器的Goroutine调度策略及性能优化

详细阐述Go调度器中Goroutine的调度策略,如M:N调度模型的原理。并且针对高并发场景下,怎样通过调整调度器相关参数或优化代码结构来提升性能,请举例说明。
10.3万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

Goroutine调度策略

  1. M:N调度模型原理
    • 概念解释:在Go的调度器中,M:N调度模型指的是N个用户级线程(Goroutine,即G)映射到M个内核级线程(M)上。这里的用户级线程是由Go运行时管理的轻量级线程,而内核级线程是操作系统内核管理的线程。
    • 调度过程
      • Goroutine创建:当一个Go程序创建一个新的Goroutine时,它被放入一个全局的Goroutine队列或者本地的Goroutine队列(每个M都有一个本地队列)。
      • M与P:M(内核线程)需要关联一个P(Processor,处理器资源,包含本地Goroutine队列等)才能运行Goroutine。P的数量决定了同一时间可以并发执行的Goroutine数量,默认情况下P的数量等于CPU核心数。
      • 调度执行:M从关联的P的本地队列中获取Goroutine来执行。如果本地队列空了,M会尝试从全局Goroutine队列中偷取一部分Goroutine到本地队列执行,或者从其他P的本地队列中偷取(work - stealing机制)。当一个Goroutine执行系统调用等阻塞操作时,M会阻塞,但P会被释放,以便其他M可以关联该P继续执行其他Goroutine。

高并发场景下提升性能的方法

  1. 调整调度器相关参数
    • 调整GOMAXPROCS
      • 作用GOMAXPROCS环境变量或函数用于设置可以同时执行的最大CPU核数,即P的数量。通过合理设置GOMAXPROCS,可以充分利用多核CPU的性能。
      • 示例:在一个CPU密集型的高并发程序中,如果服务器是8核CPU,默认情况下GOMAXPROCS会设置为8。但如果程序中有大量I/O操作,可能适当增加GOMAXPROCS的值(比如设置为16),这样在一些Goroutine进行I/O等待时,其他M可以关联更多的P执行其他Goroutine,从而提升整体性能。代码示例如下:
package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(16)
    // 这里编写高并发代码
}
  1. 优化代码结构
    • 减少锁争用
      • 问题:在高并发场景下,锁的争用会严重影响性能。例如多个Goroutine频繁地访问共享资源并使用互斥锁进行保护,会导致大量Goroutine等待锁,从而降低并发效率。
      • 优化方法:可以通过将共享资源按一定规则拆分,每个Goroutine只访问自己对应的部分,减少锁的粒度。比如在一个统计网站访问量的系统中,如果使用一个全局变量和一个互斥锁来统计总访问量,高并发下锁争用会很严重。可以将统计按时间段或IP段等维度拆分,每个Goroutine只负责更新自己对应部分的统计数据,最后再汇总。示例代码如下:
package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    counts map[string]int
    mu     sync.Mutex
}

func (c *Counter) Increment(key string) {
    c.mu.Lock()
    c.counts[key]++
    c.mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    counter := Counter{counts: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key-%d", id%10)
            counter.Increment(key)
        }(i)
    }
    wg.Wait()
    fmt.Println(counter.counts)
}
  • 使用无锁数据结构
    • 优势:在某些场景下,使用无锁数据结构(如Go标准库中的sync.Map)可以避免锁争用,提升性能。sync.Map适合高并发读写场景,它内部采用了一种更复杂的结构来避免锁的使用。
    • 示例:在一个高并发的缓存系统中,如果使用普通的map加互斥锁来实现缓存,会有锁争用问题。使用sync.Map则可以提升性能。示例代码如下:
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    var cache sync.Map
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            cache.Store(id, id*2)
        }(i)
    }
    wg.Wait()
    cache.Range(func(key, value interface{}) bool {
        fmt.Printf("Key: %d, Value: %d\n", key, value)
        return true
    })
}