【Golang】golang常见八股

  • ~3.71K 字
  1. 1. Channel
    1. 1.1. 共享内存
    2. 1.2. CSP
    3. 1.3. 死锁问题
  2. 2. 并发
    1. 2.1. 协程与线程
    2. 2.2. MPG
    3. 2.3. 资源竞争
    4. 2.4. 容器的并发安全性
    5. 2.5. sync.Once()
  3. 3. 切片
    1. 3.1. 母子切片内存共享
    2. 3.2. 内存泄漏
    3. 3.3. 函数参数需要用切片指针吗
    4. 3.4. 遍历修改切片

Channel

共享内存

全局变量下内存是共享的,多线程共享内存来通信

  • Go中原生的map不支持并发(若并发读写会报Fatal),但数组和切片以及sync.Map是支持并发的,但是数组和切片的并发结果不可预计
  • 需要加锁来访问共享内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"sync"
"time"
)

var mp sync.Map

func rwGlobalMemory() {
if value, exists := mp.Load("mykey"); exists {
fmt.Println(value)
} else {
mp.Store("mykey", "myvalue")
}
}

func main() {
go rwGlobalMemory()
go rwGlobalMemory()
go rwGlobalMemory()
go rwGlobalMemory()

time.Sleep(time.Second)
}

并发下该代码的结果是不可预测的,可能是3个可能是2个也可能是1个

CSP

Communicating Sequential Processes,即以通信的方式来共享内存,在golang中就是goroutine负责并发执行,而channel负责在goroutine之间传递数据和信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"time"
)
type Task struct {
A int
B int
}

var ch = make(chan Task, 100)

func producer() {
for i := 0; i < 10; i++ {
ch <- Task{A: i+3, B: i -8}
}
}

func consumer() {
for i := 0; i<10; i++ {
task := <- ch
sum := task.A + task.B
fmt.Println("sum:", sum)
}
}

func main() {
go producer()
go consumer()
time.Sleep(time.Second)
}

生产者和消费者共享一个channel,生产者将数据传入channel,消费者从channel中取出数据并计算,实现共享内存

如实现合并多个小文件的功能,每个协程把读出来的部分内容放到channel中,再开一个协程取出channel中的内容并合并为一个大文件

死锁问题

  • Channel满了,阻塞写;Channel空了,阻塞读

  • 阻塞后会交出CPU,执行其他协程,希望其他协程能帮自己解除阻塞

  • 若阻塞在main协程中,没有其他子协程可以执行,则可以确定希望永远等不来,会自己把自己杀掉,报Fatal error

  • 若阻塞发生在子协程中,就不会发生死锁,因为main协程给是一个值得等待的希望,会一直等待下去

并发

协程与线程

  • 一对一:一个用户的线程对应一个内核线程
  • m对n:多个用户线程和多个内核线程多对多对应

MPG

  • M:内核线程

  • P:虚拟处理器,代表M的上下文环境,是处理用户级代码逻辑的处理器,由环境变量中GOMAXPROCS决定

  • G:轻量级的线程,即goroutine

  • M要拿到P才能执行G

  • 当一个goroutine阻塞时,M会释放绑定的P,将P转移到其他空闲的M上,继续执行G队列,若该goroutine解除阻塞,如果有空闲的P,就到对应的P上执行,否则进入全局可运行队列,P会周期扫描全局可运行队列,如果为空,则从其他P的等待队列里骰一半G过来

资源竞争

多协程并发修改同一块内存时,会产生资源竞争,可以在go run或go build执行时添加-race参数检查资源竞争情况

如:n++不是原子操作,分为取出n,+1,结果赋给n,开启并发后,可能会发生脏写,解决方法可以将整个操作变为原子操作,使用atomic.AddInt32 之类的方法可以解决资源竞争

更广泛的,可以使用读写锁:

var lock sync.RWMutex:声明读写锁

lock.Lock(), lock.Unlock():加/释放写锁

lock.RLock(), lock.RUnlock():加/释放读锁

任意时刻只能加一把写锁,且不能加读锁

若没加写锁时,可以同时加多把读锁,读锁加上后不能再加写锁

容器的并发安全性

  • 数组、slice和struct允许并发修改,但可能会脏写
  • 并发修改map会panic,如果并发修改需要使用sync.Map,但是该map性能较低,若对性能要求高,可以用普通map并分段分给协程,若一定要访问同一段只能加锁
  • 工程上一般解决方法是让多个协程处理数组不同部分的数据,避免脏写

sync.Once()

在高并发场景下确保有些事情只执行一次,比如加载配置文件、关闭管道等,创建once后在对应的do函数内创建方法即可

1
2
3
4
5
6
7
8
9
type Singleton struct {}
var singleton *Singleton
var singletonOnce sync.Once
func GetSingletonInstance() *Singleton {
singletonOnce.Do(func(){
singleton = &Singleton{}
})
return singleton
}

切片

切片的底层是结构体,底层是一个结构体:

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}
  • array是一个指针,指向一个底层数组的首
  • len表示数组的长度
  • cap表示数组的预留空间,最多容纳几个元素,多出来的部分是读不出来的
  • append追加的元素放到预留的空间,并len+1,如果预留空间已经用完,则会重新申请一块更大的内存空间
1
2
arr := make([]int, 3, 5)
brr := append(arr, 8)

结果是:

它们共享底层数组,指向同一块内存空间

  • 若预留空间用完,会重新申请一块更大的内存空间,这样会把原内存空间的数据拷贝过来,因此性能会变慢

母子切片内存共享

1
2
s := make([]int, 3, 5)
sub_slice = s[1:3]
  • 刚开始子切片和母切片共享底层内存空间,修改子切片会反应到母切片中,在子切片上执行append会把新元素放到母切片预留的空间上
1
2
3
4
arr := make([]int, 3, 5)
crr = arr[0:2]
crr[1] = 8
crr = append(crr, 4)
  • 因为它们共享内存空间,因此无论是将crr的1号索引值修改为8,还是crrappend一个4,都会对arr造成影响
  • 若不断append,当超出arr的cap,crr就和arr的内存分离了,因为申请了新的内存空间,这时两者互不相干

内存泄漏

认为GC会回收的不用的内存并没有被回收,导致内存泄漏

1
2
3
4
5
func return_sub_s() []int {
parent := make([]int, total)
child := parent[begin:end]
return child
}

若parent持有一个很大的数组,占用大量内存,但子切片很小,这时如果子切片一直在使用,parent就得不到释放,函数调用多次导致内存使用增加

函数参数需要用切片指针吗

假设传入的数组cap=3

1
2
3
4
5
func changeSlice(arr *[]int) {
*arr = append(*arr, 9)
*arr = (*arr)[1:2]
(*arr)[0] = 9
}
  • 第一个追加元素,len和cap都要变(cap=6),所以要传切片指针
  • 第二个截取元素,len、pointer和cap都变(cap=5),需要传切片指针
  • 第三个修改切片元素,只改变底层数组,不需要传切片指针

遍历修改切片

Golang中for range取得的是集合中元素的拷贝

1
2
3
4
5
6
for _, ele := range arr {
ele = ele+1;
}
for i, ele := range arr {
arr[i] = ele+1;
}

第二种方式才能修改成功

赞助喵
非常感谢您的喜欢!
赞助喵