1. 协程是什么?协程与进程、线程的区别什么?
协程是编写简便、不依赖系统内核、低资源消耗的并发工具。与进程和线程相比协程有以下区别和优势
- 协程由用户代码控制,具有较小的开销
- 协程具有更高的执行效率,因为其不需要频繁的上下文切换
- 协程能够简化并发编程,使代码更易于理解和维护。
概念区分
- 进程是操作系统中运行的独立程序,每个进程都拥有独立的内存空间和资源。
- 线程是进程中的一个执行单元,同一进程内的线程共享该进程的内存和资源。
- 协程(也称作微线程或纤程)是更轻量级的线程,它们是由程序本身调度而不是操作系统调度的。
上下文切换
- 进程和线程的上下文切换由操作系统内核控制,代价较高,因为需要保存和恢复大量的状态信息。
- 协程的上下文切换由用户代码控制,代价较低,因为只需要保存和恢复很少的状态信息,这也让协程在高并发场景中表现出色。
资源消耗
- 创建和销毁进程代价最大,因为它们需要分配和释放大量的系统资源。
- 创建和销毁线程相对便宜,但仍然涉及一定的系统开销,栈是MB级别。
- 协程的创建和销毁几乎没有什么系统开销,因为它们不依赖操作系统内核,仅在用户态操作,栈是KB级别。
阻塞与非阻塞
- 进程和线程在系统调用时可能会发生阻塞,影响效率。
- 协程可以利用事件循环和非阻塞的IO操作,避免阻塞,进一步优化性能,也使得编写高效的并发代码更加容易。
代码易读性和维护性
- 使用进程和线程进行并发编程需要使用复杂的同步机制(如锁信号量等)来保证数据一致性,容易导致代码复杂且难维护。
- 协程通过顺序编写异步执行的代码,简化了并发程序的结构,使代码更直观,更容易理解和维护。
Go语言中的协程(Goroutine)
Go语言通过goroutine和channel来简化并发编程,一个goroutine通常只有几KB的栈空间,并且会根据需求动态增长,使得数以万计的goroutine运行在单个系统上也是现实可行的。另外,Go的调度器能有效地将goroutine映射到操作系统的线程上,进一步提升了并发性能。
2. 聊聊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包,其中包括一系列原子性操作,用于在不使用锁的情况下进行安全的并发操作。 原子操作用户态就可以完成,因此性能比加锁操作更好,但是这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。
3. Mutex有几种模式
mutex有两种模式:normal和starvation
- 正常模式
在正常模式中,锁的获取是非公平的,即等待锁的Goroutine不保证按照先来先服务(FIFO)的顺序获得锁。新到来的Goroutine有可能在等待时间较长的Goroutine之前获得锁。
- 饥饿模式:
在饥饿模式中,系统保证等待锁的Goroutine按照一定的公平原则获得锁,避免某些Goroutine长时间无法获取锁的情况。
4. Mutex有几种状态
- mutexLocked — 表示互斥锁的锁定状态;
- mutexWoken — 表示从正常模式被从唤醒;
- mutexStarving — 当前的互斥锁进入饥饿状态;
- waitersCount — 当前互斥锁上等待的 Goroutine 个数;
5. 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运行。
6. goroutine什么情况会发生内存泄漏?如何避免。
在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。
暂时性内存泄露
- 获取长字符串中的一段导致长字符串未释放
- 获取长slice中的一段导致长slice未释放
- 在长slice新建slice导致泄漏
string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏。
7. 如何控制Goroutine的生命周期?
- 启动:使用
go关键字可以去启动一个新的gorountine。 - 等待结束:希望主程序等待某个gorountine执行完毕后再继续执行。可以使用
sync.WaitGroup来实现等待。 - 使用通道(channel)来通知goroutine退出。
- 使用
context可以实现超时控制、取消和传递参数等功能。
8. Channel如何处理阻塞?
- 缓冲通道,在创建通道时指定缓冲区大小,即创建一个缓冲通道。当缓冲区未满时,发送数据不会阻塞。当缓冲区未空时,接收数据不会阻塞。
- select语句用于处理多个通道操作,可以用于避免阻塞。
- 使用
time.After创建一个定时器,可以在超时后执行特定的操作,避免永久阻塞。 select语句中使用default分支,可以在所有通道都阻塞的情况下执行非阻塞的操作。
9. Go语言的context是什么?有什么作用?
Go语言的context是一个标准库中的包,提供了用于上下文管理的功能,包主要用于在多个goroutine之间传递上下文信息,比如:截止时间、取消信号以及携带请求特定值。在并发编程中,它可以帮助我们更好地管理goroutine的生命周期和资源使用。常用在Web服务器API 调用、数据库操作等需要跨多个goroutine协同工作的场景中。
具体来说,context有以下几个主要作用:
- 控制并发任务的生命周期:通过在多个goroutine之间共享一个 context.Context,可以实现一处取消,处处响应,如果一个请求超时或客户端取消了请求,那么可以通过context传播取消信号,通知所有相关的goroutine停止工作,从而避免资源泄露。
- 传递请求范围内的数据:在处理HTTP请求时,context可以用来传递一些请求相关的数据,比如认证Token、用户信息等。(不建议过多使用)
- 设置超时时间:可以通过context来设置某个操作的超时时间,一旦超时,相关的goroutine就会被取消。
10. 聊聊GMP模型
概念
Go语言的GMP模型指的是 (G)Goroutines、P(Processor)、M(Machine) 的模型。这是Go语言实现并发编程时所采用的一种轻量级线程管理调度系统,通过这个模型,Go语言可以实现高效的并发处理。
- G(Goroutine): 这是Go中的轻量级线程,类似其他语言中的线程,但更轻便。每个Goroutine分配的初始内存非常小,通常在几kb量级,可以轻松创建成千上万个Goroutinue而不会对系统资源产生巨大的压力。
- P(Processor): 代表逻辑处理器,用于调度Goroutines。P提供了Goroutine执行所需的环境,包括运行队列和一些其他状态信息。
- M(Machine): 与操作系统线程一一对应,即实际的内核线程。M执行分配给它的Goroutine。
P和M的数量在Go运行时系统中是动态管理的,且可以进行动态调整。
工作流程
- 创建Goroutine: 创建一个新的Goroutine时,这个Goroutine就会被放入到一个P的运行队列中等待执行。
- 调度Goroutine: P会从它的队列中取出一个Goroutine让一个空闲的M去执行,如果M在运行某个Goroutine时,该Goroutine发生了阻塞,M就会找一个未阻塞的Goroutine来运行,从而最大化CPU的利用率。
- 协作式抢占: Go采用协作式调度,即Goroutine放弃对CPU的控制权通常发生在函数调用、进入或退出runtime某些特定代码段时。
- 工作窃取: 为了平衡不同的P负载,如果某个P的Gotoutine队列空了,它可以从其他P的队列中“窃取”一些Goroutine来执行。
- Goroutine的栈增长: Go中的Goroutine使用的是动态增长的栈空间,因此Goroutine创建时的初始内存很小,让快速创建和销毁大量Goroutine成为可能。
通过上述机制,GMP模型能高效地管理Goroutines和操作系统线程之间的关系,从而实现了高并发且轻量级的协程调度。这也是Go在实现高性能并发程序时的核心优势所在。