目录
- 01 Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量
- 02 无缓冲Chan的发送和接收是否同步
- 03 Golang并发机制以及它所使用的CSP并发模型
- 04 Golang中常用的并发模型
- 05 Go中对nil的Slice和空Slice的处理是一致的吗
- 06 协程和线程和进程的区别
- 07 Golang的内存模型中为什么小对象多了会造成GC压力
- 08 Go中数据竞争问题怎么解决
- 09 什么是channel,为什么它可以做到线程安全
- 10 Golang垃圾回收算法
- 11 GC的触发条件
- 12 Go的GPM如何调度
- 13 并发编程概念是什么
- 14 Go语言的栈空间管理是怎么样的
- 15 Goroutine和Channel的作用分别是什么
- 16 怎么查看Goroutine的数量
- 17 Go中的锁有哪些
- 18 怎么限制Goroutine的数量
- 19 Channel是同步的还是异步的
- 20 Goroutine和线程的区别
- 21 Go的Struct能不能比较
- 22 Go的defer原理是什么
- 23 Go的select可以用于什么
- 24 Go的Context包的用途是什么
- 25 Go主协程如何等其余协程完再操作
- 26 Go的Slice如何扩容
- 27 Go中的map如何实现顺序读取
- 28 Go中CAS是怎么回事
- 29 Go中的逃逸分析是什么
- 30 Go值接收者和指针接收者的区别
- 31 Go的对象在内存中是怎样分配的
- 32 栈的内存是怎么分配的
- 33 堆内存管理怎么分配的
- 35 在Go函数中为什么会发生内存泄露
- 36 Go中new和make的区别
- 37 G0的作用
- 41 Go中的http包的实现原理
- 42 Goroutine发生了泄漏如何检测
- 43 Go函数返回局部变量的指针是否安全
- 44 Go中两个Nil可能不相等吗
- 46 为何GPM调度要有P
1. Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量
channel
2. 无缓冲Chan的发送和接收是否同步
同步
3. Golang并发机制以及它所使用的CSP并发模型
Goroutine 是Golang实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine是一种运行在用户态的用户线程
go底层选择使用coroutine的出发点是因为它具有以下特点:
- 用户空间,避免了内核态和用户态的切换导致的成本.
- 可以由语言和框架层进行调度.
- 更小的栈空间允许创建大量的实例.
Golang内部有三个对象:
- P对象(processor) 代表上下文
GOMAXPROCS()
- M对象(work thread) 代表工作线程
- G对象(goroutine)
P对象对应一个M对象,碰到G对象阻塞时,会启动一个新的M对象,P对象会转到新的M对象中去运行
CSP并发模型,是通过Goroutine和Channel来实现的
4. Golang中常用的并发模型
- 通过channel通知实现并发控制
- 通过sync包中的WaitGroup实现并发控制 Add,Done,Wait,
- Context上下文,实现并发控制
5. Go中对nil的Slice和空Slice的处理是一致的吗
不一致.
empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的
6. 协程和线程和进程的区别
进程
是程序的一次执行过程,是程序在执行过程中的分配和管理资源的基本单位,每个进程都有自己的地址空间,进程是系统进行资源分配和调度的一个独立单位。线程
是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源协程
是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
7. Golang的内存模型中为什么小对象多了会造成GC压力
小对象过多会导致GC三色法消耗过多的CPU
8. Go中数据竞争问题怎么解决
- 互斥锁sync.Mutex
- CAS无锁并发解决. 争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。
9. 什么是channel,为什么它可以做到线程安全
发送一个数据到Channel和从Channel接收一个数据都是原子性的。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。
10. Golang垃圾回收算法
标记清除垃圾回收算法
- 栈扫描(开始时STW),所有对象最开始都是白色.
- 从 root开始找到所有可达对象(所有可以找到的对象),标记为灰色,放入待处理队列。
- 遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色。
- 清除(并发) 循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象
11. GC的触发条件
- 主动触发(手动触发),通过调用runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕.
- 被动触发,分为两种方式:
- 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC.
- 使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发.
12. Go的GPM如何调度
新创建的G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中,最终等待被逻辑处理器P执行即可。
当P的Local队列中没有G时,再从Global队列中获取一个G,当Global队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行相当于P之间的负载均衡。
13. 并发编程概念是什么
并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
14. Go语言的栈空间管理是怎么样的
动态地分配栈空间, 栈复制法(stack copying)。
会分配一个两倍大的内存块并把老的内存块内容复制到新的内存块里。这样做意味着当栈缩减回之前大小时,我们不需要做任何事情。栈的缩减没有任何代价。
15. Goroutine和Channel的作用分别是什么
协程,可以看作是轻量级的线程。但与线程不同的是,线程的切换是由操作系统控制的,而协程的切换则是由用户控制的。多个协程可以在多个处理器同时跑。
channel则是goroutinues之间进行通信的渠道。
16. 怎么查看Goroutine的数量
runtime.NumGoroutine()
17. Go中的锁有哪些
- 互斥锁 sync.Mutex
- 读写锁 sync.RWMutex
- sync.Map的安全的锁
18. 怎么限制Goroutine的数量
func main(){
ch = make(chan int,5) //控制并发
wg := sync.WaitGroup{}
for i:=0;i<10;i++{
wg.Add(1)
ch <-i //五个之后就会阻塞 除非有goroutine消费了chan 可以继续并发 但是上限是5
go elegance(&wg)
}
wg.Wait()
fmt.Println("end")
}
19. Channel是同步的还是异步的
异步的
Channel只由写方关闭,读方不可关闭。如果由读方关闭了,写方不知道继续写的话,就会引发panic
操作 | 一个零值nil通道 | 一个非零值但已关闭的通道 | 一个非零值且尚未关闭的通道 |
---|---|---|---|
关闭 | panic | panic | 成功关闭 |
写 | 永久阻塞 | panic | 阻塞或者成功发送 |
读 | 永久阻塞 | 永不阻塞 | 阻塞或者成功接收 |
20. Goroutine和线程的区别
- 从调度上看,goroutine的调度开销远远小于线程调度开销。
- 从栈空间上,goroutine的栈空间更加动态灵活。线程都有一个固定大小的栈内存,通常是2MB
- goroutine没有可供程序员访问的标识
21. Go的Struct能不能比较
相同struct类型的可以比较
不同struct类型的不可以比较,编译都不过,类型不匹配
22. Go的defer原理是什么
- 延迟函数的参数的值在defer语句出现时就已经确定下来了
- 延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
- 延迟函数可以操作主函数的具名返回值
23. Go的select可以用于什么
select 机制是,监听多个channel,每一个 case 是一个事件,可以是读事件也可以是写事件,随机选择一个执行,可以设置default.
24. Go的Context包的用途是什么
专门用来简化对于处理单个请求的多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作。
- 不要把Context放在结构体中,要以参数的方式传递。
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO。
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。
- Context是线程安全的,可以放心的在多个goroutine中传递。
25. Go主协程如何等其余协程完再操作
sync.WaitGroup
func main() {
var wg sync.WaitGroup
wg.Add(2) // 因为有两个动作,所以增加2个计数
go func() {
fmt.Println("Goroutine 1")
wg.Done() // 操作完成,减少一个计数
}()
go func() {
fmt.Println("Goroutine 2")
wg.Done() // 操作完成,减少一个计数
}()
wg.Wait() // 等待,直到计数为0
}
26. Go的Slice如何扩容
如果切片的容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;
一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。
如果扩容之后,还没有触及原数组的容量,那么,切片中的指针指向的位置,就还是原数组,如果扩容之后,超过了原数组的容量,那么,Go就会开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
27. Go中的map如何实现顺序读取
可以先把map中的key,通过sort包排序.
package main
import (
"fmt"
"sort"
)
func main() {
var m = map[string]int{
"hello": 0,
"morning": 1,
"keke": 2,
"jame": 3,
}
var keys []string
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
}
28. Go中CAS是怎么回事
CAS算法(Compare And Swap),是原子操作的一种, CAS算法是一种有名的无锁算法。
无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)
go中CAS操作可以有效的减少使用锁所带来的开销,但是需要注意在高并发下这是使用cpu资源做交换的
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var (
counter int32 //计数器
wg sync.WaitGroup //信号量
)
func main() {
threadNum := 5
wg.Add(threadNum)
for i := 0; i < threadNum; i++ {
go incCounter(i)
}
wg.Wait()
}
func incCounter(index int) {
defer wg.Done()
spinNum := 0
for {
// 原子操作
old := counter
ok := atomic.CompareAndSwapInt32(&counter, old, old+1)
if ok {
break
} else {
spinNum++
}
}
fmt.Printf("thread,%d,spinnum,%d\n", index, spinNum)
}
29. Go中的逃逸分析是什么
如果变量的作用域不会扩大并且其行为或者大小能够在编译的时候确定,一般情况下都是分配到栈上,否则就可能发生内存逃逸分配到堆上。
- 发送指针的指针或值包含了指针到channel 中,由于在编译阶段无法确定其作用域与传递的路径,所以一般都会逃逸到堆上分配。
- slices 中的值是指针的指针或包含指针字段。一个例子是类似[]*string 的类型。这总是导致 slice 的逃逸。即使切片的底层存储数组仍可能位于堆栈上,数据的引用也会转移到堆中。
- slice 由于 append 操作超出其容量,因此会导致 slice 重新分配。这种情况下,由于在编译时 slice 的初始大小的已知情况下,将会在栈上分配。如果 slice 的底层存储必须基于仅在运行时数据进行扩展,则它将分配在堆上。
- 调用接口类型的方法。接口类型的方法调用是动态调度,实际使用的具体实现只能在运行时确定。考虑一个接口类型为 io.Reader 的变量 r。对 r.Read(b) 的调用将导致 r 的值和字节片b的后续转义并因此分配到堆上。
- 尽管能够符合分配到栈的场景,但是其大小不能够在在编译时候确定的情况,也会分配到堆上.
30. Go值接收者和指针接收者的区别
- 如果方法的接收者是
值类型
,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者; - 如果方法的接收者是
指针类型
,则调用者修改的是指针指向的对象本身。
通常我们使用指针作为方法的接收者的理由:
- 使用指针方法能够修改接收者指向的值。
- 可以避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
31. Go的对象在内存中是怎样分配的
Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。
32. 栈的内存是怎么分配的
栈和堆只是虚拟内存上2块不同功能的内存区域:
- 栈在高地址,从高地址向低地址增长。
- 堆在低地址,从低地址向高地址增长。
栈和堆相比优势:
栈的内存管理简单,分配比堆上快。
栈的内存不需要回收,而堆需要,无论是主动free,还是被动的垃圾回收,这都需要花费额外的CPU。 栈上的内存有更好的局部性,堆上内存访问就不那么友好了,CPU访问的2块数据可能在不同的页上,CPU访问数据的时间可能就上去了。
33. 堆内存管理怎么分配的
35. 在Go函数中为什么会发生内存泄露
- 预期能很快被释放的内存附着在长期对象上
- goroutine泄漏,如果一个程序持续不断地产生新的goroutine、且不结束已经创建的goroutine并复用这部分内存,就会造成内存泄漏的现象
36. Go中new和make的区别
值类型
:int,float,bool,string,struct,array.
变量直接存储值,分配栈区的内存空间,这些变量所占据的空间在函数被调用完后会自动释放。
引用类型
:slice,map,channel,值类型对应的指针.
变量存储的是一个地址(或者理解为指针),指针指向内存中真正存储数据的首地址。内存通常在堆上分配,通过GC回收。
new
该方法的参数要求传入一个类型,而不是一个值,它会申请一个该类型大小的内存空间,并会初始化为对应的零值,返回指向该内存空间的一个指针make
也是用于内存分配,但是和new不同,只用来引用对象slice、map和channel的内存创建,它返回的类型就是类型本身,而不是它们的指针类型。
37. G0的作用
- g0 总是第一个创建的 goroutine。
- g0 其他的一些“职责”有:创建 goroutine、垃圾回收相关的工作(例如 stw、扫描 goroutine 的执行栈、一些标识清扫的工作、栈增长)等等。
41. Go中的http包的实现原理
42. Goroutine发生了泄漏如何检测
通过Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源
43. Go函数返回局部变量的指针是否安全
在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上.
44. Go中两个Nil可能不相等吗
Go中两个Nil可能不相等。
两个接口值比较时,会先比较 T,再比较 V。 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
46. 为何GPM调度要有P
- 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。
- 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。