Go中常见面试题

前言

本章给大家带来一些Go中常见面试题,常见面试也是我们工作中容易错误一些知识点。 例如:golang里的数组和切片的区别。

golang里的数组和切片有了解过吗?

数组长度是固定的,而切片是可变长的。可以把切片看作是对底层数组的封装,每个切片的底层数据结构中,一定会包含一个数组。数组可以被称为切片的底层数组,切片也可以被看作对数组某一连续片段的引用。因此,Go中切片属于引用类型,而数组属于值类型,通过内建函数len,可以取得数组和切片的长度。通过内建函数cap,可以得到数组和切片的容量。但是数组的长度和容量是相等的,并且都不可变,而且切片容量是有变化规律的。

数组和切片的关系:
切片一旦初始化, 切片始终与保存其元素的基础数组相关联。因此,切片会和与其拥有同一基础数组的其他切片共享存储 ; 相比之下,不同的数组总是代表不同的存储。

数组和切片的区别

  1. 切片的长度可能在执行期间发生变化 ,而数组的长度不能变化,可以把切片看成一个长度可变的数组。

  2. 数组作为函数参数是进行值传递的,函数内部改变传入的数组元素值不会影响函数外部数组的元素值; 切片作为函数的参数是进行的指针传递,函数内部改变切片的值会影响函数外部的切片元素值。

  3. 数组可以比较,切片不能比较(对底层数组的引用)。

例题: 说出下面的返回值

package main

import "fmt"

func main() {
    s := []int{1, 2}
    s1 := []int{1, 2}
    s2 := make([]int, 2, 3)
    a := [2]int{1, 2}
    test(s, s1, s2, a)
    fmt.Printf("outter=> s: %v, s1: %v, s2: %v, a: %v\n", s, s1, s2, a)
}
func test(s []int, s1 []int, s2 []int, a [2]int) {
    s = append(s, 3, 4, 5, 6, 7)
    s[0] = 9

    s1[0] = 9

    s2 = append(s2, 8)
    s2[0] = 9

    a[0] = 9
    fmt.Printf("inner=> s: %v, s1:%v, s2:%v, a: %v\n", s, s1, s2, a)
}

输出结果:
inner=> s: [9 2 3 4 5 6 7], s1:[9 2], s2:[9 0 8], a: [9 2]
outter=> s: [1 2], s1: [9 2], s2: [9 0], a: [1 2]

介绍一下通道

如果说goroutine是Go程序并发的执行体,通道就是它们之间的连接。通道可以使一个goroutine发送特定值到另一个goroutine的通信机制。每一个通道都是一个具体类型的通过,叫做通道的元素类型。例如一个具有int类型元素的通道写为chan int

通道是一个用map创建的数据结构的引用。当复制或者作为参数传递到一个函数时,复制的是引用,这样调用者和被调用者都引用同一份数据结构。和其他引用类型一样,通道的零值是nil

通道有两个主要操作:发送(send)和接收(receive),两者统称为通信。send语句从一个goroutine传输一个值到另一个在执行接收表达式的goroutine。两个操作都使用<-操作符书写。发送语句中,通道和值分别在<-的左右两边。在接收表达式中,<-放在通道操作数前面,在接收表达式中,其结果未被使用也是合法的。

ch <- x        //发送语句
x = <-ch    //接收语句
<-ch        //接收语句,丢弃结果

通道支持第三个操作:关闭 (close),它设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了;关闭后的发送操作将导致宕机。在一个已经关闭的通道上进行接收操作,将获取所有已经发送的值,直到通道为空;这时任何接收操作会立即完成,同时获取到一个通道元素对应的零值。通过调用内置的close函数来关闭通道:

close(ch)

根据通道的容量,可以将通道分为无缓冲通道和缓冲通道

  • 无缓冲通道: ch = make(chan int) 、ch = make(chan int, 0)
  • 有缓冲通道: ch = make(chan int, 3)

根据通道传输方向,还可以通道分为双向通道,只读通道和只写通道

  • 只读通道: 只能发送的通道,允许发送但不允许接收 chan<- int
  • 只写通道: 只能接收的通道,允许接收但不允许发送 <-chan int

简答版:

通道类型的值本身就是并发安全的。在声明并初始化一个通道时,可以使用内建函数make,传给这个函数第一个参数为通道具体类型的字面量(如:chan int),还可以接一个可选的整形参数作为通道的容量,但是这个整形数据不能小于零。

通道相当与一个先进先出(FIFO)的队列,各个元素严格按照发送顺序排列,先被发送的一定会被先接收。使用操作符表示<-

如果定义通道时未指定通道的长度,那么该通道的长度为0,没有缓冲,即发送一个数据之后,通道就会阻塞,直到该元素被接收。如果定义的长度为nn为正整数),那么通道的长度即为n

channel实现方式

channel实现方式/原理/概念/底层实现

  • Go语言提供了一种不同的并发模型–通信顺序进程(communicating sequential processes,CSP)。
  • 设计模式:通过通信的方式共享内存
  • channel收发操作遵循先进先出(FIFO)的设计
type hchan struct {

    qcount   uint           // channel中的元素个数
    dataqsiz uint           // channel中循环队列的长度
    buf      unsafe.Pointer // channel缓冲区数据指针
    elemsize uint16            // buffer中每个元素的大小
    closed   uint32            // channel是否已经关闭,0未关闭
    elemtype *_type // channel中的元素的类型
    sendx    uint   // channel发送操作处理到的位置
    recvx    uint   // channel接收操作处理到的位置
    recvq    waitq  // 等待接收的sudog(sudog为封装了goroutine和数据的结构)队列由于缓冲区空间不足而阻塞的Goroutine列表
    sendq    waitq  // 等待发送的sudogo队列,由于缓冲区空间不足而阻塞的Goroutine列表
    lock mutex   // 一个轻量级锁

}

channel创建: ch := make(chan int, 3)

  • 创建channel实际上就是在内存中实例化了一个hchan结构体,并返回一个chan指针
  • channel在函数间传递都是使用的这个指针,这就是为什么函数传递中无需使用channel的指针,而是直接用channel就行了,因为channel本身就是一个指针

channel发送数据: ch <- 1 ch <- 2

  • 检查 recvq 是否为空,如果不为空,则从 recvq 头部取一个 goroutine,将数据发送过去,并唤醒对应的 goroutine 即可。
  • 如果 recvq 为空,则将数据放入到 buffer 中。
  • 如果 buffer 已满,则将要发送的数据和当前 goroutine 打包成 sudog 对象放入到 sendq中。并将当前 goroutine 置为 waiting 状态。

channel接收数据:<-ch <-ch

  • 检查sendq是否为空,如果不为空,且没有缓冲区,则从sendq头部取一个goroutine,将数据读取出来,并唤醒对应的goroutine,结束读取过程。

  • 如果sendq不为空,且有缓冲区,则说明缓冲区已满,则从缓冲区中首部读出数据,把sendq头部的goroutine数据写入缓冲区尾部,并将goroutine唤醒,结束读取过程。

  • 如果sendq为空,缓冲区有数据,则直接从缓冲区读取数据,结束读取过程。

  • 如果sendq为空,且缓冲区没数据,则只能将当前的goroutine加入到recvq,并进入waiting状态,等待被写goroutine唤醒。

channel规则:

操作空channel已关闭channel活跃中的channel
close(ch)panicpanic成功关闭
ch<- v永远阻塞panic成功发送或阻塞
v,ok = <-ch永远阻塞不阻塞成功接收或阻塞

channel和锁的对比

并发问题可以用channel解决也可以用Mutex解决,但是它们的擅长解决的问题有一些不同。

channel关注的是并发问题的数据流动,适用于数据在多个协程中流动的场景。

mutex关注的是是数据不动,某段时间只给一个协程访问数据的权限,适用于数据位置固定的场景。

向为nil的channel发送数据会怎么样

空通道即无缓冲通道。无缓冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作,这时值传送完成,两个goroutine都可以继续执行。相反,如果接收操作先执行,接收方gorountine将阻塞,直到另一个goroutine在同一个通道上发送一个值。

使用无缓冲通道进行的通信导致发送和接收goroutine同步化。因此,无缓冲通道也称为同步通道。当一个值在无缓冲通道上传递时,接收值后发送方goroutine才被再次唤醒。

WaitGroup使用点

① Add一个负数

如果计数器的值小于0会直接panic

② Add在Wait之后调用

比如一些子协程开头调用Add结束调用Wait,这些 Wait无法阻塞子协程。正确做法是在开启子协程之前先Add特定的值。

③ 未置为0就重用

WaitGroup可以完成一次编排任务,计数值降为0后可以继续被其他任务所用,但是不要在还没使用完的时候就用于其他任务,这样由于带着计数值,很可能出问题。

④ 复制waitgroup

WaitGroupnocopy字段,不能被复制。也意味着WaitGroup不能作为函数的参数。

go struct 能不能比较

需要具体情况具体分析,如果struct中含有不能被比较的字段类型,就不能被比较,如果struct中所有的字段类型都支持比较,那么就可以被比较。

不可被比较的类型:
① slice,因为slice是引用类型,除非是和nil比较
② map,和slice同理,如果要比较两个map只能通过循环遍历实现
③ 函数类型

其他的类型都可以比较。

还有两点值得注意:

  • 结构体之间只能比较它们是否相等,而不能比较它们的大小。
  • 只有所有属性都相等而属性顺序都一致的结构体才能进行比较。

读写锁底层是怎么实现的

读写锁的底层是基于互斥锁实现的。

  • 为什么有读写锁,它解决了什么问题?(使用场景)
  • 它的底层原理是什么?

在这里我会结合 Go 中的读写锁 RWMutex 进行介绍。

我们通过与 Mutex 对比得出答案。Mutex 是不区分 goroutine 对共享资源的操作行为的,在读操作、它会上锁,在写操作,它也会上锁,当一段时间内,读操作居多时,读操作在 Mutex 的保护下也不得不变为串行访问,对性能的影响也就比较大了。

RWMutex 读写锁的诞生为了区分读写操作,在进行读操作时,goroutine 就不必傻傻的等待了,而是可以并发地访问共享资源,将串行读变成了并行读,提高了读操作的性能。

读写锁针对解决一类问题:readers-writes,同时有多个读或者多个写操作时,只要有一个线程在执行写操作,其他的线程都不能进行读操作。

读写锁其实有三种工作模型:

  • Read-perferring 优先读设计,可能会导致写饥饿
  • Write-prferring 优先写设计,避免写饥饿
  • 不指定优先级 不区分优先级,解决饥饿问题

Go 中的读写锁,工作模型是 Write-prferring 方案。

简洁答案:

  1. 读写锁解决问题

主要应用于写操作少,读操作多的场景。读写锁满足以下四条规则。

  • 写锁需要阻塞写锁:一个协程拥有写锁时,其他协程写锁定需要阻塞;
  • 写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞;
  • 读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻塞;
  • 读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁。
  1. 读写锁底层实现
    读写锁内部仍有一个互斥锁,用于将多个写操作隔离开来,其他几个都用于隔离读操作和写操作。

2个协程交替打印字母和数字

package main

import (
    "fmt"
)

func main() {
    limit := 26
    numChan := make(chan int, 1)
    charChan := make(chan int, 1)
    mainChan := make(chan int, 1)
    charChan <- 1
    go func() {
        for i := 0; i < limit; i++ {
            <-charChan
            fmt.Printf("%c\n", 'a'+i)
            numChan <- 1
        }
    }()
    go func() {
        for i := 0; i < limit; i++ {
            <-numChan
            fmt.Println(i)
            charChan <- 1
        }
        mainChan <- 1
    }()
    <-mainChan
    close(charChan)
    close(numChan)
    close(mainChan)
}

goroutine与线程的区别?

一个线程可以有多个协程。

线程、进程都是同步机制;而协程是异步 。

协程可以保留上一次调用时的状态,当过程重入时,相当于进入了上一次的调用状态,协程是需要线程来承载运行的,所以协程并不能取代线程,线程是被分割的CPU资源,协程是组织好的代码流程。

讲一讲 GMP 模型

三个字母的含义

  • G(Goroutine)G 就是我们所说的 Go 语言中的协程 Goroutine 的缩写,相当于操作系统中的进程控制块。其中存着 goroutine 的运行时栈信息,CPU 的一些寄存器的值以及执行的函数指令等。

  • M(Machine):代表一个操作系统的主线程,对内核级线程的封装,数量对应真实的 CPU 数。一个 M 直接关联一个 os 内核线程,用于执行 G。M 会优先从关联的 P 的本地队列中直接获取待执行的 G。M 保存了 M 自身使用的栈信息、当前正在 M上执行的 G 信息、与之绑定的 P 信息。

  • P(Processor):Processor 代表了 M 所需的上下文环境,代表 M 运行 G 所需要的资源。是处理用户级代码逻辑的处理器,可以将其看作一个局部调度器使 go 代码在一个线程上跑。当 P 有任务时,就需要创建或者唤醒一个系统线程来执行它队列里的任务,所以 PM 是相互绑定的。总的来说,P 可以根据实际情况开启协程去工作,它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 PP 中还包含了可运行的 G 队列。

首先呢,GMP 这三个字母的含义分别是 Goroutine,Machine,Processor。这个Goroutine,相当于操作系统中的进程控制块。其中存着 goroutine 的运行时栈信息,CPU 的一些寄存器的值以及执行的函数指令等。Machine就是代表了一个操作系统的主线。M 结构体中,保存了 M 自身使用的栈信息、当前正在 M上执行的 G 信息、与之绑定的 P 信息。M 直接关联一个 os 内核线程,用于执行 G。(这里思考一个这个模型的图片回答),这个 M 做的事情就是从关联的 P 的本地队列中直接获取待执行的 G。剩下的 Processor 是代表了 M 所需的上下文环境,代表 M 运行 G 所需要的资源。当 P 有任务时,就需要创建或者唤醒一个系统线程来执行它队列里的任务。在GMP调度模型中,P 的个数就是 GOMAXPROCS,是可以手动设置的,但一般不修改,GOMAXPOCS 默认值是当前电脑的核心数,单核CPU就只能设置为1,如果设置>1,在 GOMAXPOCS 函数中也会被修改为1。总的来说,这个 P 结构体的主要的任务就是可以根据实际情况开启协程去工作。

GO中深拷贝和浅拷贝

深入理解Go语言的深拷贝与浅拷贝:优化你的数据复制策略"

深拷贝︰拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。

实现深拷贝的方式:

  • copy(slice2, slice1)
  • 遍历slice进行append赋值

浅拷贝∶拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。

实现浅拷贝的方式:引用类型的变量,默认赋值操作就是浅拷贝。 如slice2 := slice1

引用类型与值类型

引用类型 变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配。通过 GC 回收。包括 指针、slice 切片、管道 channel、接口 interface、map、函数等。

值类型是 基本数据类型,int,float,bool,string, 以及数组和 struct 特点:变量直接存储值,内存通常在栈中分配,栈在函数调用后会被释放

对于引用类型的变量,我们不光要声明它,还要为它分配内容空间

于值类型的则不需要显示分配内存空间,是因为go会默认帮我们分配好

new()

func new(Type) *Type, new(): 对类型进行内存分配,入参为类型,返回为类型的指针,指向分配类型的内存地址。

make()

func make(t Type, size ...IntegerType) Type

make()也是用于内存分配的,但是和new不同,它只用于channelmap以及切片的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。

简而言之make()用于初始化slice, map, channel等内置数据结构

channel的应用场景

channel适用于数据在多个协程中流动的场景,有很多实际应用:

① 任务定时

比如超时处理:

select {
    case <-time.After(time.Second):

定时任务:

select {
    case <- time.Tick(time.Second)

② 解耦生产者和消费者

可以将生产者和消费者解耦出来,生产者只需要往channel发送数据,而消费者只管从channel中获取数据。

③ 控制并发数

以爬虫为例,比如需要爬取1w条数据,需要并发爬取以提高效率,但并发量又不过过大,可以通过channel来控制并发规模,比如同时支持5个并发任务:

ch := make(chan int, 5)
for _, url := range urls {
    go func() {
            ch <- 1
            worker(url)
            <- ch
    }
}

go的GC

标记清理 -> 三色标记发 -> 混合写屏障

  1. 标记清除:
    此算法主要有两个主要的步骤:

标记(Mark phase)
清除(Sweep phase)

第一步,找出不可达的对象,然后做上标记。
第二步,回收标记好的对象。

操作非常简单,但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 stop the world。

也就是说,这段时间程序会卡在哪儿。故中文翻译成 卡顿.

标记-清扫(Mark And Sweep)算法存在什么问题?
标记-清扫(Mark And Sweep)算法这种算法虽然非常的简单,但是还存在一些问题:

STW,stop the world;让程序暂停,程序出现卡顿。

标记需要扫描整个heap

清除数据会产生heap碎片
这里面最重要的问题就是:mark-and-sweep 算法会暂停整个程序。

  1. 三色并发标记法:
    首先:程序创建的对象都标记为白色。
    gc开始:扫描所有可到达的对象,标记为灰色。
    从灰色对象中找到其引用对象标记为灰色,把灰色对象本身标记为黑色。
    监视对象中的内存修改,并持续上一步的操作,直到灰色标记的对象不存在。
    此时,gc回收白色对象。
    最后,将所有黑色对象变为白色,并重复以上所有过程。

  2. 混合写屏障:
    注意:
    当gc进行中时,新创建一个对象. 按照三色标记法的步骤,对象会被标记为白色,这样新生成的对象最后会被清除掉,这样会影响程序逻辑。

golang引入写屏障机制.可以监控对象的内存修改,并对对象进行重新标记。

gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

slice用copy和左值进行初始化的区别

  1. copy(slice2, slice1)实现的是深拷贝。拷贝的是数据本身,创造一个新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。

同样的还有:遍历slice进行append赋值

  1. slice2 := slice1实现的是浅拷贝。拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。默认赋值操作就是浅拷贝。

channel是否线程安全

channel为什么设计成线程安全?

不同协程通过channel进行通信,本身的使用场景就是多线程,为了保证数据的一致性,必须实现线程安全。

channel如何实现线程安全的?

channel的底层实现中, hchan结构体中采用Mutex锁来保证数据读写安全。在对循环数组buf中的数据进行入队和出队操作时,必须先获取互斥锁,才能操作channel数据。

go的map是线程安全的吗?

  • 线程安全:对map进行并发读写时,如果程序能正常运行并能得到预期的结果。Map默认不是并发安全的,并发读写时程序会panic

  • map为什么不支持线程安全?和场景有关,官方认为大部分场景不需要多个协程进行并发访问,如果为小部分场景加锁实现并发访问,大部分场景将付出加锁代价(性能降低)。

  • 实现:
    1)加读写锁(map+sync.RWMutex
    2)使用Go提供的sync.Map(内部加了锁)

Go语言Slice是否线程安全

Go语言实现线程安全常用的几种方式:

1.互斥锁;
2.读写锁;
3.原子操作;
4.sync.once;
5.sync.atomic;
6.channel

slice底层结构并没有使用加锁等方式,不支持并发读写,所以并不是线程安全的,使用多个goroutine对类型为slice的变量进行操作,每次输出的值大概率都不会一样,与预期值不一致;
slice在并发执行中不会报错,但是数据会丢失。

make可以初始化哪些结构

通过make创建对象 make只能创建slice 、channel、 map。

new和make对比:

  • make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
  • new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  • new 分配的空间被清零。make 分配空间后,会进行初始化;
  • make 函数只用于 map,slice 和 channel,并且不返回指针

go 深拷贝发生在什么情况下?切片的深拷贝是怎么做的?

深拷贝,浅拷贝概念

  1. 深拷贝(Deep Copy):

拷贝的是数据本身,创造一个样的新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值。既然内存地址不同,释放内存地址时,可分别释放。

  1. 浅拷贝(Shallow Copy):

拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。释放内存地址时,同时释放内存地址。参考来源
在go语言中值类型赋值都是深拷贝,引用类型一般都是浅拷贝:

值类型的数据,默认全部都是深拷贝:Array、Int、String、Struct、Float,Bool
引用类型的数据,默认全部都是浅拷贝:Slice,Map
对于引用类型,想实现深拷贝,不能直接 := ,而是要先开辟地址空间(new) ,再进行赋值。

怎么进行(切片的)深拷贝?

可以使用 copy() 函数来进行深拷贝,copy 不会进行扩容,当要复制的 slice 比原 slice 要大的时候,只会移除多余的。

func main() {
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{6, 7, 8}
    copy(slice2, slice1) // 复制slice1的前3个元素到slice2中
    fmt.Println(slice1, slice2)
    copy(slice1, slice2) // 复制slice2的3个元素到slice1的前3个位置
    fmt.Println(slice1, slice2)
}

使用 append() 函数来进行深拷贝,append 会进行扩容(这里涉及到的就是 Slice 的扩容机制 )。

func main() {
    a := []int{1, 2, 3}
    b := make([]int, 0)
    b = append(b, a[:]...)
    fmt.Println(a, b)
    a[1] = 1000
    fmt.Println(a, b)
    fmt.Printf("%p,%p", a, b)
}

Go 中切片扩容的策略:

  • 首先判断,如果新申请容量大于 2 倍的旧容量,最终容量就是新申请的容量
  • 否则判断,如果旧切片的长度小于 1024,则最终容量就是旧容量的两倍
  • 否则判断,如果旧切片长度大于等于 1024,则最终容量从旧容量开始循环 增加原来的 1/4, 直到最终容量大于等于新申请的容量
  • 如果最终容量计算值溢出,则最终容量就是新申请容量

注意:如果 sliceappend() 过程中没有发生扩容,那么修改就在原来的内存中,如果发生了扩容,就修改在新的内存中。

空结构体占不占内存空间? 为什么使用空结构体?

空结构体是没有内存大小的结构体。
通过 unsafe.Sizeof() 可以查看空结构体的宽度,代码如下:

var s struct{}
fmt.Println(unsafe.Sizeof(s)) // prints 0

准确的来说,空结构体有一个特殊起点: zerobase 变量。zerobase是一个占用 8 个字节的uintptr全局变量。每次定义 struct {} 类型的变量,编译器只是把zerobase变量的地址给出去。也就是说空结构体的变量的内存地址都是一样的。
空结构体的使用场景主要有三种:

  • 实现方法接收者:在业务场景下,我们需要将方法组合起来,代表其是一个 ”分组“ 的,便于后续拓展和维护。

  • 实现集合类型:在 Go 语言的标准库中并没有提供集合(Set)的相关实现,因此一般在代码中我们图方便,会直接用 map 来替代:type Set map[string]struct{}

  • 实现空通道:在 Go channel 的使用场景中,常常会遇到通知型 channel,其不需要发送任何数据,只是用于协调 Goroutine 的运行,用于流转各类状态或是控制并发情况。

如果在两个不同的结构体类型之间进行比较(地址不同),则一定不能进行比较,除非强制转换成相同的结构体类型。

如果在一个结构体中,需要看情况进行比较。在 Go 语言中,当其基本类型包含:slicemapfunction 时,是不能比较的。

GPM模型

golang gmp模型,全局队列中的G会不会饥饿,为什么?P的数量是多少?能修改吗?M的数量是多少?

  1. 全局队列中的G不会饥饿。 因为线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。所以全局队列中的G总是能被消费掉.

  2. P的数量可以理解为最大为本机可执行的cpu的最大数量。
    通过runtime.GOMAXPROCS(runtime.NumCPU())设置。
    runtime.NumCPU()方法返回当前进程可用的逻辑cpu数量。

服务器能开多少个M由什么决定? 服务器能开多少个P由什么决定?

由于M必须持有一个P才可以运行Go代码,所以同时运行的M个数,也即线程数一般等同于CPU的个数,以达到尽可能的使用CPU而又不至于产生过多的线程切换开销。

P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。

Go语⾔本身是限定M的最⼤量是10000,可以在runtime/debug包中的SetMaxThreads函数来修改设置

P的个数在程序启动时决定,默认情况下等同于CPU的核数

程序中可以使用 runtime.GOMAXPROCS() 设置P的个数,在某些IO密集型的场景下可以在一定程度上提高性能。

一般来讲,程序运行时就将GOMAXPROCS大小设置为CPU核数,可让Go程序充分利用CPU。在某些IO密集型的应用里,这个值可能并不意味着性能最好。理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继续占满CPU。但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔的,所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些,或许会有好的效果。

关于我
loading