go

Go

1. 大家都说Go语言比较快,那它是怎么做到的呢

go 的并发模型使用的是协程的方式,协程是用户级别的线程,相比较于操作系统的级别的线程,上下文切换成本更低,内存占用空间更小,操作系统级别的线程栈空间通常是 2M,而协程的栈空间为 2K,它的栈空间会动态地增加,除此之外 go 语言内部针对协程使用的 GPM 调度模型,可以充分利用 CPU 的计算资源,使程序响应速度更快。因此在性能相同的机器上,go 语言编写的程序可以支持更多的并发,响应速度更快。

除此之外,go 语言的快还提现在学习简单,新人学习 2 到 3 周就可以写出高性能的代码,使用简单,创建协程只需使用 go 关键字就行了。

部署简单,直接打包成对应平台的二进制文件,就可以执行了。

2. Go的Channel有用过吗,说一下你对于Channel的理解

在 go 中有句这样的话,不要通过共享内存来通信,而要通过通信来共享内存,而 channel 就是 go 中不同协程间通信的媒介。

3. 有缓冲和无缓冲Channel的区别

有缓存通道在发送和接受数据时可以到缓存中看看是否可以放置数据和取出数据,如果可以就不会阻塞。否则就会阻塞。

而无缓存通道在发送和接受数据时必须有相应的接受和发送数据的协程,否则会一直阻塞,因此无缓存通道也叫做同步协程。

5. Go的Context有用过吗,说一下你对于Context的理解

context 主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。信号传递的方式主要以下四种:

  • 父任务主动取消 WithCancel()

  • 超时取消 WithTimeout()

  • 截止时间取消 WithDeadline()

  • 传递上下文数据 WithValue()

7. 用Go简单写一个生产者/消费者的例子


package main

import (

"fmt"

"sync"

)

type Consumer struct {

NO int

c <- chan string

}

func (consumer *Consumer)consume() {

for data := range consumer.c {

fmt.Printf("consumer[%d] consume data- '%s'\n",consumer.NO,data)

}

}

type Productor struct {

NO int

c chan <- string

}

func (productor *Productor)product() {

for i := 0; i < 100; i++ {

productor.c<- fmt.Sprintf("data[%d]- created byproductor-[%d]",i,productor.NO)

}

}

func main() {

productorWg := sync.WaitGroup{}

ch := make(chan string,1)

for i := 0; i < 10; i++ {

productorWg.Add(1)

go func(no int) {

defer productorWg.Done()

p := Productor{no,ch}

p.product()

}(i)

}

consumerWg := sync.WaitGroup{}

for i := 0; i < 100; i++ {

consumerWg.Add(1)

go func(no int) {

defer consumerWg.Done()

c := Consumer{no,ch}

c.consume()

}(i)

}

go func() {

productorWg.Wait()

close(ch)

}()

productorWg.Wait()

fmt.Println("task finish")

}

8. make和new的区别是什么

new 的作用是初始化一个指向类型零值的指针(*T),make 的作用是为 slice,map 或 chan 初始化并返回引用(T)。

如果 new 去初始化 map,slice,channel 时返回的是 nil 的指针

go 中参数传递都是值传递,没有引用传递,所谓的引用传递,只是传递后效果和传引用一样,根源是传递的结构体中有指针,所以传了指针。

9. Go里面的map遍历两遍,得到的结果会是一样的吗

不一样,map 的遍历是没有顺序的,因为在遍历的过程中加入了随机种子,遍历的开始点是不一样的,之所以这么做是因为之后版本的 map 遍历可能会无序,为了兼容之前的版本,因此在较低版本就随机了,减少开发者的依赖。

如果想要有顺序的话,可以使用 slice 保存 key,然后遍历 slice 来保证有序。

10. Go里面想要可并发使用的map该怎么做

1. 加锁

2. 使用 sync.map

11. Go里面的锁有了解过吗,是否可重入?自旋?

锁分为互斥锁和读写锁。

读写锁是只要加锁别的协程都不可再获得锁资源。

读写锁是如果某一个协程加了读锁,其他协程还可以加读锁,但是无法拿到写锁,如果某一个协程加了写锁,则其他协程读锁写锁都不可以拿到,比较适合读多写少的场景。

并且其中读写锁的读锁是可重入锁,互斥锁和读写锁的写锁是不可重入锁。不可嵌套使用,否则会造成死锁。

4. Channel底层实现是什么结构

channel 的结构如下所示


type hchan struct {

 qcount  uint  // total data in the queue;chan中的元素总数

 dataqsiz uint  // size of the circular queue;底层循环数组的size

 buf unsafe.Pointer // points to an array of dataqsiz elements,指向底层循环数组的指针,只针对有缓冲的channel

 elemsize uint16 //chan中元素的大小

 closed  uint32 //chan是否关闭

 elemtype *_type // element type;元素类型

 sendx uint  // send index;已发送元素在循环数组中的索引

 recvx uint  // receive index;已接收元素在循环数组中的索引

 recvq waitq // list of recv waiters,等待接收消息的goroutine队列

 sendq waitq // list of send waiters,等待发送消息的goroutine队列

 // lock protects all fields in hchan, as well as several

 // fields in sudogs blocked on this channel.

 //

 // Do not change another G's status while holding this lock

 // (in particular, do not ready a G), as this can deadlock

 // with stack shrinking.

 lock mutex

}

其中主要的部分是缓存的 buffer,它是一个循环链表,在有缓存通道中保存缓存的数据,其次就是已发送数据的下标,和已接受数据的下标,用来表示 buffer 中的数据使用情况,还有发送数据阻塞的协程链表和接受数据阻塞的协程链表,还有 lock 对象,通道每次发送和接受数据时都会进行加锁。

6. Go的GMP机制有了解过吗,谈一下你的理解

从以下几个方面进行介绍

1. GPM 名词解释

M: Machine 工作线程,它由操作系统进行调用,是程序调度的最小单位

P: Processor,处理器,包含有 Go 协程的代码,runQueue,并且进行 go 调度

G: Goroutine,每个 go 关键字都会创建一个协程,其实就是一个方法加参数

2. GPM 模型的设计理念,为什么有 GPM 模型

应用在多核cpu实现并行处理的方案主要是多进程与多线程两种方式,多进程模型相对简单,但是有着资源开销大及进程间通信成本高的问题。多线程模型相对复杂,会有死锁,线程安全,模型复杂等问题,但却因为资源开销及易于管理等优点适用于对于性能要求较高的应用。

Golang采用的是多线程模型,更详细的说他是一个两级线程模型,但它对系统线程(内核级线程)进行了封装,暴露了一个轻量级的协程goroutine(用户级线程)供用户使用,而用户级线程到内核级线程的调度由golang的runtime负责,调度逻辑对外透明。

goroutine的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。

3. GPM 是如何进行调度的

一个M会和一个P进行绑定,一个P会和很多G进行绑定,这些G会存储在P的本地 runqueue,然后提供给 M进行执行。

P的个数默认和CPU的核数相同,主要是为了充分利用CPU的计算能力。

如果当前P中没有G时,会从其他P或则全局队列中窃取G来执行。

如果本地P队列满了,会把G放入全局队列中。

在P调度过程中有概率从全局队列中获取G执行,否则全局队列中的G就无法执行了。

4. 如果阻塞了怎么办

场景 1:由于原子、互斥量或通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine;

场景2: 由于网络请求和 IO 操作导致 Goroutine 阻塞,当前协程会被移动到网络轮询器并且处理异步网络系统调用。然后,M 可以从 LRQ 执行另外的 Goroutine。此时,G2 就被上下文切换到 M 上了。等 G1 网络系统调用完成就会再次回到 P 上等待队列中。

场景 3:当调用一些系统方法的时候,如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 Goroutine 将阻塞当前 M。识别出 G1 已导致 M1 阻塞,此时,调度器将 M1 与 P 分离,同时也将 G1 带走。调度器引入新的 M2 来服务 P。此时,可以从 LRQ 中选择 G2 并在 M2 上进行上下文切换。阻塞的系统调用完成后:G1 可以移回 LRQ 并再次由 P 执行。如果这种情况再次发生,M1 将被放在旁边以备将来重复使用。

场景 4:如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。

Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

只要下次这个 Goroutine 进行函数调用,那么就会被强占,同时也会保护现场,然后重新放入 P 的本地队列里面等待下次执行。

https://blog.csdn.net/csdniter/article/details/112175118

https://blog.csdn.net/enoch612/article/details/105587749

https://blog.csdn.net/nextvary/article/details/79656055?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-5.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-

12. Go的垃圾处理机制有了解过吗,三色标记、写屏障是怎么回事

三色标记法,是 go 垃圾回收的一种算法实现,是标记-请求法的一个改良版本,目的是为了减少 STW 的时间,增加程序的吞吐量。

其中的三色是指白色、灰色、黑色,白色表示未被引用的对象,灰色表示已被引用但是没有查询其直接引用情况的对象,黑色表示已被引用切遍历了其直接引用的对象。

第一步:把当前所有堆内存中的对象标记为白色,这个过程会 STW,然后从程序的栈出发,查看当前直接引用的对象,然后把他标记为灰色,

第二步:然后再查找灰色对象直接引用的对象,并把这些对象标记为灰色对象,并把当前灰色对象变为黑色对象,重复这个过程,直到没有灰色对象。

第三部:把目前的白色对象进行清除

但是在标记清除的时候,会出现多标或则漏标的情况,多标的话,下次再回收即可,对程序的正确性没有影响,但是漏标就会导致程序出错。

漏标的情况发生在标记过程中,灰色对象引用的白色对象被黑色对象引用了,此时这个白色对象就无法变灰色,变黑色,就会导致已被引用的对象被回收,就会导致程序出错。

不难分析,漏标只有同时满足以下两个条件时才会发生:

条件一:灰色对象断开了白色对象的引用(直接或间接的引用);即灰色对象 原来成员变量的引用 发生了变化。

条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。

为了防止这样的事情发生就只需破坏这两个条件即可

所以有了写屏障,就是在写操作之后进行的一些逻辑处理:主要有两种:

方法1:在灰色对象发生变化的时候,把原有的引用保存下来,到时候清理的时候还按原来的结构清理即可

方法2: 在黑色对象引用白色对象时,保存在来,之后把黑色对象保存的白色对象进行遍历即可。

还有读屏障,就是在进行读操作时的一些逻辑处理。就是当黑色对象读取成员变量时都记录下来,之后再遍历这些对象,这也是针对条件二的方法。

参看文章:

https://www.jianshu.com/p/12544c0ad5c1

https://studygolang.com/articles/22104?fr=sidebar

你可能感兴趣的:(go)