公告

微信

欢迎大家私信交流

Skip to content

并发编程

无缓冲channel和有缓冲的channel的区别

  1. 无缓冲channel(unbuffered channel):无缓冲channel在发送(send)操作时需要有接收者(receiver)同时准备好接收数据,否则发送操作会被阻塞,直到有接收者准备好。同样地,在接收操作时,需要有发送者准备好发送数据,否则接收操作会被阻塞。

  2. 有缓冲channel(buffered channel):有缓冲channel在创建时会指定一个缓冲区大小,可以在该缓冲区未满的情况下发送数据,而不会立即阻塞发送操作。只有当缓冲区已满时,发送操作才会被阻塞。同样地,在接收操作时,只有当缓冲区为空时,接收操作才会被阻塞。

总结:

  • 无论是无缓冲channel还是有缓冲channel,在没有对应的发送者或接收者时,都会导致阻塞。
  • 对于无缓冲channel,发送和接收操作必须同时准备好。这种同步的特性使得无缓冲channel适用于两个goroutine之间的数据交换,确保发送和接收操作的同步性。
  • 对于有缓冲channel,缓冲区满或为空时才会阻塞。有缓冲channel允许发送和接收操作在不同的时间进行,它们之间不需要同时准备好。这种异步的特性使得有缓冲channel适用于解耦发送和接收操作的场景,可以提高并发性能和灵活性。

协程泄漏

为什么有协程泄漏

协程创建后,没有得到释放。

主要原因:

  1. 缺少接收器,发送阻塞
  2. 缺少发送器,接收阻塞
  3. 死锁,多个协程竞争资源
  4. 创建的协程没有回收

协程什么情况会发生内存泄漏

在Go中内存泄露分为暂时性内存泄露和永久性内存泄露。

暂时性内存泄露

  • 获取长字符串中的一段导致长字符串未释放
  • 获取长slice中的一段导致长slice未释放
  • 在长slice新建slice导致泄漏
    1. 获取长slice中的一段导致长slice未释放:
      • 当从一个长slice中获取一段子slice时,如果没有正确处理,子slice可能会持有对原长slice的引用,导致原长slice无法被释放。
      • 这种情况下,子slice会持有原长slice的底层数组的引用,即使子slice被丢弃,原长slice的底层数组也无法被释放,造成内存泄漏。
      • 避免内存泄漏的方法是使用copy函数将子slice复制到一个新的slice中,而不是直接引用原长slice。
    2. 在长slice新建slice导致泄漏:
      • 当在一个长slice上再次使用切片操作创建一个新的slice时,新的slice会共享原长slice的底层数组,导致原长slice无法被释放。
      • 这种情况下,新的slice会持有原长slice的底层数组的引用,即使新的slice被丢弃,原长slice的底层数组也无法被释放,造成内存泄漏。
      • 避免内存泄漏的方法是使用copy函数将新的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

go
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")
}

使用有缓冲的通道

通过控制通道的缓冲大小,可以限制同时执行的协程数量。

go
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

go
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协议,使用了二进制传输和多路复用的特性,具有较低的延迟、高并发和高效的数据压缩等优势。它还提供了多种身份验证和安全机制,可以保障通信的安全性和可靠性。

上次更新于: