并发编程
无缓冲channel和有缓冲的channel的区别
无缓冲channel(unbuffered channel):无缓冲channel在发送(send)操作时需要有接收者(receiver)同时准备好接收数据,否则发送操作会被阻塞,直到有接收者准备好。同样地,在接收操作时,需要有发送者准备好发送数据,否则接收操作会被阻塞。
有缓冲channel(buffered channel):有缓冲channel在创建时会指定一个缓冲区大小,可以在该缓冲区未满的情况下发送数据,而不会立即阻塞发送操作。只有当缓冲区已满时,发送操作才会被阻塞。同样地,在接收操作时,只有当缓冲区为空时,接收操作才会被阻塞。
总结:
- 无论是无缓冲channel还是有缓冲channel,在没有对应的发送者或接收者时,都会导致阻塞。
- 对于无缓冲channel,发送和接收操作必须同时准备好。这种同步的特性使得无缓冲channel适用于两个goroutine之间的数据交换,确保发送和接收操作的同步性。
- 对于有缓冲channel,缓冲区满或为空时才会阻塞。有缓冲channel允许发送和接收操作在不同的时间进行,它们之间不需要同时准备好。这种异步的特性使得有缓冲channel适用于解耦发送和接收操作的场景,可以提高并发性能和灵活性。
协程泄漏
为什么有协程泄漏
协程创建后,没有得到释放。
主要原因:
- 缺少接收器,发送阻塞
- 缺少发送器,接收阻塞
- 死锁,多个协程竞争资源
- 创建的协程没有回收
协程什么情况会发生内存泄漏
在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。
暂时性内存泄露
- 获取长字符串中的一段导致长字符串未释放
- 获取长slice中的一段导致长slice未释放
- 在长slice新建slice导致泄漏
- 获取长slice中的一段导致长slice未释放:
- 当从一个长slice中获取一段子slice时,如果没有正确处理,子slice可能会持有对原长slice的引用,导致原长slice无法被释放。
- 这种情况下,子slice会持有原长slice的底层数组的引用,即使子slice被丢弃,原长slice的底层数组也无法被释放,造成内存泄漏。
- 避免内存泄漏的方法是使用copy函数将子slice复制到一个新的slice中,而不是直接引用原长slice。
- 在长slice新建slice导致泄漏:
- 当在一个长slice上再次使用切片操作创建一个新的slice时,新的slice会共享原长slice的底层数组,导致原长slice无法被释放。
- 这种情况下,新的slice会持有原长slice的底层数组的引用,即使新的slice被丢弃,原长slice的底层数组也无法被释放,造成内存泄漏。
- 避免内存泄漏的方法是使用copy函数将新的slice复制到一个新的slice中,而不是直接引用原长slice。
- 获取长slice中的一段导致长slice未释放:
为避免这两种情况下的内存泄漏,需要注意在处理slice时避免共享底层数组的引用,而是使用copy函数创建新的slice。确保在不需要使用的时候及时释放不再需要的内存,避免长期持有对底层数组的引用。 同时,合理使用defer语句、关闭资源、避免循环引用等方法也有助于避免内存泄漏的发生。
string相比切片少了一个容量的cap字段,可以把string当成一个只读的切片类型。获取长string或者切片中的一段内容,由于新生成的对象和老的string或者切片共用一个内存空间,会导致老的string和切片资源暂时得不到释放,造成短暂的内存泄漏.
永久性内存泄露
- goroutine永久阻塞而导致泄漏
- time.Ticker未关闭导致泄漏
- 不正确使用Finalizer(Go版本的析构函数)导致泄漏
go可以限制运行时操作系统线程的数量吗?常见的goroutine操作函数有哪些?(不懂)
可以,使用runtime.GOMAXPROCS(num int)可以设置线程数目。该值默认为CPU逻辑核数,如果设的太大,会引起频繁的线程切换,降低性能。
- runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行
- runtime.Goexit(),调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。runtime.Goexit在终止当前goroutine前会先执行此goroutine的还未执行的defer语句。请注意千万别在主函数调用runtime.Goexit,因为会引发panic。
如何控制协程数目
以下是使用不同方法控制协程数量的代码示例:
使用sync.WaitGroup
package main
import (
"fmt"
"sync"
"time"
)
func main() {
// sync.WaitGroup是一个计数器,可以用来等待一组协程的完成。
var wg sync.WaitGroup
numWorkers := 5
for i := 0; i < numWorkers; i++ {
// 1. 使用Add()方法增加计数器的值,表示要等待的协程数量
wg.Add(1)
go worker(&wg)
}
// 3. 使用Wait()方法阻塞当前协程,直到所有协程都执行完成。
wg.Wait()
fmt.Println("All workers completed")
}
func worker(wg *sync.WaitGroup) {
// 2. 在每个协程执行完成时,调用Done()方法减少计数器的值。
defer wg.Done()
// 模拟工作
time.Sleep(1 * time.Second)
fmt.Println("Worker completed")
}
使用有缓冲的通道
通过控制通道的缓冲大小,可以限制同时执行的协程数量。
package main
import (
"fmt"
"time"
)
func main() {
// 1. 创建一个有固定大小的通道,并在通道中缓冲一定数量的任务。
numWorkers := 5
taskCh := make(chan int, numWorkers)
// 2. 使用一个循环来从通道中接收任务,并在每个任务上启动一个协程来执行
for i := 0; i < numWorkers; i++ {
go worker(taskCh)
}
for i := 0; i < numWorkers; i++ {
taskCh <- i
}
close(taskCh)
time.Sleep(2 * time.Second)
fmt.Println("All workers completed")
}
func worker(taskCh chan int) {
for task := range taskCh {
// 模拟工作
time.Sleep(1 * time.Second)
fmt.Println("Worker", task, "completed")
}
}
使用semaphore
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
numWorkers := 5
maxConcurrency := 2
semaphore := make(chan struct{}, maxConcurrency)
for i := 0; i < numWorkers; i++ {
wg.Add(1) // 增加等待组的计数器
go worker(&wg, semaphore)
}
wg.Wait() // 等待所有协程完成
fmt.Println("All workers completed")
}
func worker(wg *sync.WaitGroup, semaphore chan struct{}) {
defer wg.Done() // 减少等待组的计数器
semaphore <- struct{}{} // 获取信号量
// 模拟工作
time.Sleep(1 * time.Second)
fmt.Println("Worker completed")
<-semaphore // 释放信号量
}
go竞态条件了解吗?
所谓竞态竞争,就是当两个或以上的goroutine访问相同资源时候,对资源进行读/写。
比如var a int = 0
,有两个协程分别对a+=1,我们发现最后a不一定为2。这就是竞态竞争。
通常我们可以用go run -race xx.go来进行检测
解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁
如果若干个goroutine,有一个panic会怎么做?
有一个panic,那么剩余goroutine也会退出,程序退出。
如果不想程序退出,那么必须通过调用 recover() 方法来捕获 panic 并恢复将要崩掉的程序。
defer可以捕获goroutine的子goroutine吗?
不可以。它们处于不同的调度器P中。
对于子goroutine,必须通过 recover() 机制来进行恢复,然后结合日志进行打印(或者通过channel传递error)
grpc是什么?
gRPC是一种基于Go的远程过程调用(RPC)框架。
RPC框架的目标是使远程服务调用变得简单和透明,它屏蔽了底层的传输方式(如TCP或UDP)、序列化方式(如XML/JSON/二进制)和通信细节。
通过使用gRPC,服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。
gRPC使用Protocol Buffers作为接口定义语言(IDL)和数据序列化格式,它定义了服务接口和消息类型。通过定义接口和消息类型,可以自动生成相应的客户端和服务器端代码,简化了开发过程。 gRPC基于HTTP/2协议,使用了二进制传输和多路复用的特性,具有较低的延迟、高并发和高效的数据压缩等优势。它还提供了多种身份验证和安全机制,可以保障通信的安全性和可靠性。