ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 一、Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量? > `Golang`中`Goroutine`可以通过`Channel`进行安全读写共享变量。 ## 二、无缓冲 Chan 的发送和接收是否同步? >ch := make(chan int)   无缓冲的channel由于没有缓冲发送和接收需要同步。 >ch := make(chan int, 2) 有缓冲channel不要求发送和接收操作同步。 * channel无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据。 * channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。 实例: >`c1:=make(chan int)`  无缓冲 `c2:=make(chan int,1)`有缓冲 > c1<-1 无缓冲的 不仅仅是 向`c1`通道放`1`而是 一直要有别的协程`<-c1`接手了 这个参数,那么`c1<-1`才会继续下去,要不然就一直阻塞着。 而`c2<-1`则不会阻塞,因为缓冲大小是`1`只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞。 无缓冲的 就是一个送信人去你家门口送信 ,你不在家 他不走,你一定要接下信,他才会走。无缓冲保证信能到你手上 有缓冲的 就是一个送信人去你家仍到你家的信箱 转身就走 ,除非你的信箱满了 他必须等信箱空下来。有缓冲的 保证 信能进你家的邮箱 ## 三、go语言的并发机制以及它所使用的CSP并发模型 ## 四、Golang 中常用的并发模型 Golang 中常用的并发模型有三种: #### 1、通过channel通知实现并发控制 无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。 从上面无缓冲的通道定义来看,发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。 ``` func main() { ch := make(chan struct{}) go func() { fmt.Println("start working") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("finished") } ``` 当主`goroutine`运行到`<-ch`接受`channel`的值的时候,如果该`channel`中没有数据,就会一直阻塞等待,直到有值。 这样就可以简单实现并发控制 #### 2、通过`sync`包中的`WaitGroup`实现并发控制 Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成。在WaitGroup里主要有三个 方法: * Add, 可以添加或减少 goroutine的数量。 * Done, 相当于Add(-1)。 * Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0。 在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。 在每一个 goroutine 完成后 Done() 表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回。 ``` func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() } ``` 在`Golang`官网中对于`WaitGroup`介绍是`A WaitGroup must not be copied after first use`,在`WaitGroup`第一次使用后,不能被拷贝 应用示例: ``` func main(){ wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Printf("i:%d", i) wg.Done() }(wg, i) } wg.Wait() fmt.Println("exit") } ``` 它提示所有的`goroutine`都已经睡眠了,出现了死锁。这是因为`wg`给拷贝传递到了`goroutine`中,导致只有`Add`操作,其实 Done操作是在`wg`的副本执行的。 因此`Wait`就死锁了。 这个第一个修改方式:将匿名函数中`wg`的传入类型改为`*sync.WaitGroup`,这样就能引用到正确的`WaitGroup`了。 这个第二个修改方式:将匿名函数中的`wg`的传入参数去掉,因为`Go`支持闭包类型,在匿名函数中可以直接使用外面的`wg`变量 ## 3、 在`Go 1.7`以后引进的强大的`Context`上下文,实现并发控制 通常,在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。 比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。 所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。 它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。 `context`包主要是用来处理多个`goroutine`之间共享数据,及多个`goroutine`的管理。 `context`包的核心是`struct Context`,接口声明如下: ``` // A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} } ``` Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号 Err() 在Done() 之后,返回context 取消的原因。 Deadline() 设置该context cancel的时间点 Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。 Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。 一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。 其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。 典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。 ## 五、JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗? 首先`JSON`标准库对`nil slice`和`空 slice`的处理是不一致 通常错误的用法,会报数组越界的错误,因为只是声明了`slice`,却没有给实例化的对象。 ~~~go var slice []int slice[1] = 0 ~~~ 此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。 empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下: >slice := make([]int,0) >slice := []int{} 当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。 总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的。 ## 六、协程,线程,进程的区别 进程 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。 线程 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。 协程 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 ## 七、互斥锁,读写锁,死锁问题是怎么解决 互斥锁 互斥锁就是互斥变量mutex,用来锁住临界区的. 条件锁就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行;读写锁,也类似,用于缓冲区等临界资源能互斥访问的。 读写锁 通常有些公共数据修改的机会很少,但其读的机会很多。并且在读的过程中会伴随着查找,给这种代码加锁会降低我们的程序效率。读写锁可以解决这个问题。 ![](https://img.kancloud.cn/38/e5/38e533229caa79f0d832d8c1a57a8d27_291x121.png) 注意:写独占,读共享,写锁优先级高 死锁 一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。 另外一种情况是:若线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。 死锁产生的四个必要条件: * 互斥条件:一个资源每次只能被一个进程使用 * 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 * 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 * 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。 a. 预防死锁 可以把资源一次性分配:(破坏请求和保持条件) 然后剥夺资源:即当某进程新的资源未满足时,释放已占有的资源(破坏不可剥夺条件) 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件) b. 避免死锁 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。 c. 检测死锁 首先为每个进程和每个资源指定一个唯一的号码,然后建立资源分配表和进程等待表. d. 解除死锁 当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有. e. 剥夺资源 从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态. f. 撤消进程 可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止.所谓代价是指优先级、运行代价、进程的重要性和价值等。 ## 八、Data Race(数据竞争)问题怎么解决?能不能不加锁解决这个问题? 同步访问共享数据是处理数据竞争的一种有效的方法。golang在1.1之后引入了竞争检测机制,可以使用 go run -race 或者 go build -race来进行静态检测。 其在内部的实现是,开启多个协程执行同一个命令, 并且记录下每个变量的状态. 竞争检测器基于C/C++的ThreadSanitizer 运行时库,该库在Google内部代码基地和Chromium找到许多错误。这个技术在2012年九月集成到Go中,从那时开始,它已经在标准库中检测到42个竞争条件。现在,它已经是我们持续构建过程的一部分,当竞争条件出现时,它会继续捕捉到这些错误。 竞争检测器已经完全集成到Go工具链中,仅仅添加-race标志到命令行就使用了检测器。 ``` go test -race mypkg // 测试包 go run -race mysrc.go // 编译和运行程序 go build -race mycmd // 构建程序 go install -race mypkg // 安装程序 ``` 要想解决数据竞争的问题可以使用互斥锁sync.Mutex,解决数据竞争(Data race),也可以使用管道解决,使用管道的效率要比互斥锁高。 ## 九、什么是channel,为什么它可以做到线程安全? Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication),Channel也可以理解是一个先进先出的队列,通过管道进行通信。 Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的。 ## 十、Epoll原理 [推荐](https://blog.csdn.net/daaikuaichuan/article/details/83862311?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165353338916781685325199%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=165353338916781685325199&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-83862311-null-null.142^v10^control,157^v12^new_style&utm_term=Epoll%E5%8E%9F%E7%90%86&spm=1018.2226.3001.4187) ## 十一、Golang GC(垃圾清除) 时会发生什么? 引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而不必关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。 常用的垃圾回收的方法: #### 1、引用计数(reference counting) 这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。 这种方法的优点是实现简单,并且内存的回收很及时。这种算法在内存比较紧张和实时性比较高的系统中使用的比较广泛,如ios cocoa框架,php,python等。 但是简单引用计数算法也有明显的缺点: * 频繁更新引用计数降低了性能。 一种简单的解决方法就是编译器将相邻的引用计数更新操作合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时通过扫描堆栈确认是否还有临时对象引用而决定是否释放。等等还有很多其他方法,具体可以参考这里。 * 循环引用。 当对象间发生循环引用时引用链中的对象都无法得到释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。当然这也增加了垃圾回收的复杂度。 #### 2、 标记-清除(mark and sweep) 标记-清除(mark and sweep)分为两步,标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。 #### 3、分代搜集(generation) java的jvm 就使用的分代回收的思路。在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新创建的对象存放在称为新生代(young generation)中(一般来说,新生代的大小会比 老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被提升(promotion)到老年代中(这里用到了一个分类的思路,这个是也是科学思考的一个基本思路)。 因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。 Golang GC 时会发生什么? Golang 1.5后,采取的是“非分代的、非移动的、并发的、三色的”标记清除垃圾回收算法。 golang 中的 gc 基本上是标记清除的过程: ![](https://img.kancloud.cn/b6/5c/b65c05a87fe19e388c7a5e248c4e6d65_2030x930.png) gc的过程一共分为四个阶段: 1. 栈扫描(开始时STW) 2. 第一次标记(并发) 3. 第二次标记(STW) 4. 清除(并发) 整个进程空间里申请每个对象占据的内存可以视为一个图,初始状态下每个内存对象都是白色标记。 1. 先STW,做一些准备工作,比如 enable write barrier。然后取消STW,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理。 2. 第一轮先扫描root对象,包括全局指针和 goroutine 栈上的指针,标记为灰色放入队列。 3. 第二轮将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象; 4. 第三轮再次STW,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了write barrier(写屏障)去记录Golang gc 优化的核心就是尽量使得 STW(Stop The World) 的时间越来越短。 ## 十二、并发编程概念是什么? 并行是指两个或者多个事件在同一时刻发生;并发是指两个或多个事件在同一时间间隔发生。 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群 并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的“同时执行”。 并发编程是指在一台处理器上“同时”处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。