分布式缓存系统(Go) (opens new window)是不错的Go的入门项目,做完不仅可以在简历上添加一个项目,还可以活学活用这些八股:Go的基础知识,线程进程、锁、并发、缓存测罗,内存分配,日志监控等等。
# Go并发
# 进程、线程、协程
进程、线程和协程都是并发编程的概念
进程是操作系统分配资源的基本单位,每个进程都有自己的独立内存空间,不同进程之间的数据不能直接共享, 通常通过进程间通信(IPC)来进行数据交换,例如管道、消息队列等。
线程是操作系统调度的最小执行单位,同一进程的不同线程共享相同的内存空间,可以直接访问共享数据。
协程是轻量级的用户态线程,由Go调度器进行管理,协程的创建和销毁比线程更为轻量,可以很容易地创建大量的协程。协程之间通过通信来共享数据,而不是通过共享内存。这通过使用通道(channel)等机制来实现。
# 进程、线程的区别
- 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
- 切换:线程上下文切换比进程上下文切换要快得多。
- 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
- 系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O 设备等,OS 所付出的开销显著大于在创建或撤销线程时的开 销,进程切换的开销也远大于线程切换的开销。
# 协程和线程的区别
- 线程和进程都是同步机制,而协程是异步机制。
- 线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
- 一个线程可以有多个协程,一个进程也可以有多个协程。
- 协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。
- 协程能保留上一次调用时的状态。
# 并行和并发的区别
- 并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务 在执行。单核处理器可以做到并发。比如有两个进程 A 和 B,A 运行一个时间片之后,切换到 B,B 运行一个时间片之后又切换到 A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。
- 并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。
# Go语言并发模型
Go语言的并发模型建立在goroutine和channel之上。其设计理念是共享数据通过通信而不是通过共享来实现
Goroutines 是Go中的轻量级线程,由Go运行时(runtime)管理。与传统线程相比,goroutines的创建和销毁开销很小。程序可以同时运行多个goroutines,它们共享相同的地址空间。
Goroutines之间的通信通过channel(通道)实现。通道提供了一种安全、同步的方式,用于在goroutines之间传递数据。使用通道可以避免多个goroutines同时访问共享数据而导致竞态条件的问题。
多路复用:
select语句允许在多个通道操作中选择一个执行。这种方式可以有效地处理多个通道的并发操作,避免了阻塞。互斥锁和条件变量
- Go提供了
sync包,其中包括Mutex(互斥锁)等同步原语,用于在多个goroutines之间进行互斥访问共享资源。 sync包还提供了Cond(条件变量),用于在goroutines之间建立更复杂的同步。
- Go提供了
原子操作: Go提供了
sync/atomic包,其中包括一系列原子性操作,用于在不使用锁的情况下进行安全的并发操作。
# 什么是go routine
goroutine(协程)是一种轻量级的线程,由Go运行时(runtime)管理,一个典型的 Go 程序可能会同时运行成千上万个 goroutine,Goroutines 使得程序可以并发执行,而无需显式地创建和管理线程。通过关键字 go 可以启动一个新的 goroutine,例如:go someFunction()。
每个 goroutine 都有自己的独立栈空间,这使得它们之间的数据不容易互相干扰。与传统的多线程编程相比,使用 goroutines 不需要开发者显式地进行线程的创建、销毁和同步。Go 运行时会自动处理这些事务。
# 如何控制 goroutine 的生命周期
- 启动
使用关键字 go 可以启动一个新的 goroutine。
go func() {
// goroutine 的代码逻辑
}()
2
3
- 等待结束
希望主程序等待某个 goroutine 执行完毕后再继续执行。可以使用 sync.WaitGroup 来实现等待。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(1) // 添加一个等待的 goroutine
go func() {
defer wg.Done() // goroutine 完成时调用 Done 减少计数
// goroutine 的代码逻辑
fmt.Println("Goroutine executing...")
}()
// 等待所有 goroutine 完成
wg.Wait()
fmt.Println("Main goroutine exiting.")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 使用通道(channel)来通知 goroutine 退出
package main
import (
"fmt"
"time"
)
func main() {
quit := make(chan bool)
go func() {
defer fmt.Println("Goroutine exiting...")
// goroutine 的代码逻辑
time.Sleep(time.Second * 2)
quit <- true // 发送退出通知
}()
// 主 goroutine 等待退出通知
<-quit
fmt.Println("Main goroutine exiting.")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 使用context包
Go 标准库中的 context 可以实现超时控制、取消、传递参数等功能。
# Go语言中的Channel是什么, 有哪些用途,如何处理阻塞
Channel(通道)是用于在goroutines之间进行通信的一种机制。通道提供了一种并发安全的方式来进行goroutines之间的通信。通过通道,可以避免在多个goroutines之间共享内存而引发的竞态条件问题,因为通道的读写是原子性的。
# 用途
数据传递: 主要用于在goroutines之间传递数据,确保数据的安全传递和同步。
同步执行: 通过Channel可以实现在不同goroutines之间的同步执行,确保某个goroutine在另一个goroutine完成某个操作之前等待。
消息传递: 适用于实现发布-订阅模型或通过消息进行事件通知的场景。
多路复用: 使用
select语句,可以在多个Channel操作中选择一个非阻塞的执行,实现多路复用。
# 如何处理阻塞
缓冲通道,在创建通道时指定缓冲区大小,即创建一个缓冲通道。当缓冲区未满时,发送数据不会阻塞。当缓冲区未空时,接收数据不会阻塞。
select语句用于处理多个通道操作,可以用于避免阻塞。使用
time.After创建一个定时器,可以在超时后执行特定的操作,避免永久阻塞。select语句中使用default分支,可以在所有通道都阻塞的情况下执行非阻塞的操作。
# 什么是互斥锁(mutex)?在什么情况下会用到它们?
互斥锁是一种用于控制对共享资源访问的同步机制。它确保在任意时刻只有一个线程能够访问共享资源,从而避免了多个线程同时对资源进行写操作导致的数据竞争和不一致性。
在并发编程中,多个线程(或者Goroutines)可能同时访问共享的数据,如果不进行同步控制,可能导致以下问题:
竞态条件(Race Condition): 多个线程同时修改共享资源,导致最终结果依赖于执行时机,可能引发不确定的行为。
数据不一致性: 多个线程同时读写共享资源,可能导致数据不一致,破坏了程序的正确性。
互斥锁通过在临界区(对共享资源的访问区域)中使用锁来解决这些问题。基本上,当一个线程获得了互斥锁时,其他线程需要等待该线程释放锁后才能获得锁。这确保了在任一时刻只有一个线程能够进入临界区。
在Go语言中,互斥锁通常使用sync包中的Mutex类型来实现。以下是一个简单的示例:
package main
import (
"fmt"
"sync"
)
var counter int
var mutex sync.Mutex
func increment(wg *sync.WaitGroup) {
defer wg.Done()
// 互斥锁加锁
mutex.Lock()
counter++
// 互斥锁解锁
mutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Counter:", counter)
}
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
mutex.Lock()用于加锁,mutex.Unlock()用于解锁。这确保了counter的并发访问是安全的,避免了竞态条件。
需要注意的是,在使用互斥锁时,要确保在临界区内的代码执行时间较短,以减小锁的持有时间,从而提高程序的并发性能。过长的锁持有时间可能导致其他线程被阻塞,降低并发性。
# Mutex有几种模式
mutex有两种模式:normal 和 starvation
- 正常模式
在正常模式中,锁的获取是非公平的,即等待锁的 Goroutine 不保证按照先来先服务(FIFO)的顺序获得锁。新到来的 Goroutine 有可能在等待时间较长的 Goroutine 之前获得锁。
- 饥饿模式:
在饥饿模式中,系统保证等待锁的 Goroutine 按照一定的公平原则获得锁,避免某些 Goroutine 长时间无法获取锁的情况。
# Mutex有几种状态
mutexLocked — 表示互斥锁的锁定状态;
mutexWoken — 表示从正常模式被从唤醒;
mutexStarving — 当前的互斥锁进入饥饿状态;
waitersCount — 当前互斥锁上等待的 Goroutine 个数;
# 无缓冲的 channel 和有缓冲的 channel 的区别?
对于无缓冲区channel:
发送的数据如果没有被接收方接收,那么**发送方阻塞;**如果一直接收不到发送方的数据,接收方阻塞;
有缓冲的channel:
发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。
# Go什么时候发生阻塞?阻塞时调度器会怎么做。
- 用于原子、互斥量或通道操作导致goroutine阻塞,调度器将把当前阻塞的goroutine从本地运行队列LRQ换出,并重新调度其它goroutine;
- 由于网络请求和IO导致的阻塞,Go提供了网络轮询器(Netpoller)来处理,后台用epoll等技术实现IO多路复用。
其它回答:
- channel阻塞:当goroutine读写channel发生阻塞时,会调用gopark函数,该G脱离当前的M和P,调度器将新的G放入当前M。
- 系统调用:当某个G由于系统调用陷入内核态,该P就会脱离当前M,此时P会更新自己的状态为Psyscall,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。
- 系统监控:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。
- 主动让出:由于是协作式调度,该G会主动让出当前的P(通过GoSched),更新状态为Grunnable,该P会调度队列中的G运行。
# goroutine什么情况会发生内存泄漏?如何避免。
在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。
暂时性内存泄露
- 获取长字符串中的一段导致长字符串未释放
- 获取长slice中的一段导致长slice未释放
- 在长slice新建slice导致泄漏
string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏
# CAP 理论,为什么不能同时满足
CAP 理论是分布式系统设计中的三个基本属性,它们分别是一致性(Consistency)、可用性(Availability)、和分区容错性(Partition Tolerance)。CAP 理论由计算机科学家 Eric Brewer 在2000年提出。
- 一致性(Consistency):
- 一致性要求系统在所有节点上的数据是一致的。即,如果在一个节点上修改了数据,那么其他节点应该立即看到这个修改。这意味着在任何时刻,不同节点上的数据应该保持一致。
- 可用性(Availability):
- 可用性要求系统能够对用户的请求做出响应,即使在出现节点故障的情况下仍然保持可用。可用性意味着系统在出现故障时仍然能够提供服务,尽管可能是部分服务。
- 分区容错性(Partition Tolerance):
- 分区容错性是指系统在面对网络分区的情况下仍能够正常工作。即,当节点之间的网络出现故障或无法通信时,系统仍能够保持一致性和可用性。
CAP 理论提出的是在分布式系统中这三个属性不能同时被满足。这是由于在分布式系统中,网络的不确定性和延迟会导致无法同时满足一致性、可用性和分区容错性。
# 切片类型Slice是并发安全的吗? (考点:并发下的Slice)【简单】
# go语言中的切片并不是并发安全的,
- 共享底层数组:多个切片可以共享同一个底层数组,一个goroutine对数组更改,会影响其他引用数组的切片
- 并发读写:多个goroutine对同一个切片读写,可能会造成竞态条件
- 扩容操作:切片的扩容操作并不是原子性的,多个goroutine同时对一个切片进行append操作,可能会造成数据丢失或覆盖
# 解决方法
- 使用同步原语:比如使用
sync.Mutex或sync.RWMutex来保护切片操作。 - 拷贝切片:每个goroutine操作自己的切片拷贝,避免直接操作共享的切片。
- 通道传递:通过通道传递切片的拷贝,而不是直接传递切片本身。
# go⾥⾯的map是并发安全的吗?如何并发安全。(考点:并发下的map)【简单】
Go里面,原生的map类型并不是并发安全的。一旦有多个goroutine同时读写同一个map,就可能遇到数据竞争和不一致的问题。
要让map在并发环境下安全使用,可以这么操作:
- 使用Mutex:可以结合sync.Mutex或者sync.RWMutex来手动管理对map的访问。读写锁RWMutex更适合读多写少的场景。
- 使用sync.Map:Go标准库提供了一个sync.Map类型。它是为并发设计的。它提供了安全的Load、Store和Delete等方法来操作映射。
- 避免共享:如果可能,尽量让每个goroutine使用自己的map拷贝,然后通过通道传递数据。避免直接共享同一个map。

评论
验证登录状态...