Goroutine堆栈大小使用调研

837 阅读8分钟

Goroutine堆栈调研

协程栈简介

Go 协程(Goroutine)具有堆栈,每个协程都拥有自己的堆栈。与线程相比,协程的堆栈更加轻量级,协程的堆栈大小默认为 2KB,可以通过 runtime.GOMAXPROCS() 函数来调整。

在这里插入图片描述

go 协程栈的位置

  • go 的协程栈位于go 的堆内存上
  • go堆内存位于操作系统的虚拟内存上(这里是操作系统为每一个进程分配的虚拟内存,不是物理机中虚拟内存的概念)

当一个协程被创建时,它会分配一个固定大小的堆栈,用于存储其局部变量、参数、返回值和函数调用信息等。当协程调用一个函数时,该函数的局部变量和参数会被存储在堆栈中,当函数返回时,这些变量会被清除。

由于协程的堆栈是固定大小的,因此需要注意防止堆栈溢出。当协程的堆栈空间不足时,Go 运行时系统会自动扩展协程的堆栈,但如果扩展失败,程序就会崩溃。因此,建议在编写协程时,尽可能减少其堆栈使用,避免过深的递归调用。

在go 协程里添加很大的切片会怎样

在 Go 协程中添加很大的切片可能会导致内存消耗过多,并且可能会影响应用程序的性能。

在 Go 中,每个协程都有自己的堆栈,堆栈的大小默认为 2KB。如果您创建了一个非常大的切片,并在协程中使用它,它将被存储在堆上,而不是在协程的栈上。这意味着,如果您的切片非常大,它可能会占用大量的内存,并且可能会导致应用程序崩溃或变得非常缓慢。

package main
​
import (
  "fmt"
  "log"
  "net/http"
  "time""github.com/arl/statsviz"
)
​
func main() {
  ch := make(chan int, 1)
​
  go func() {
    // 创建一个非常大的切片
    slice := make([]int, 100000000)
    for i := range slice {
      slice[i] = i
    }
    fmt.Println("goroutine finished")
    ch <- 1
  }()
​
  fmt.Println("waiting for goroutine to finish...")
  
  go func() {
    for {
      select {
      case <-ch:
        <-ch
      }
    }
  }()
  fmt.Println("done")
  //time.Sleep 等待一段时间,以便观察内存使用情况
  statsviz.RegisterDefault()
  log.Println(http.ListenAndServe(":6060", nil))
  time.Sleep(10 * time.Second)
}
​

http://localhost:6060/debug/statsviz/中观察内存使用情况

在上面的示例中,我们创建了一个非常大的切片(100000000),并在一个协程中进行填充。在主协程中,我们等待该协程完成,并打印出“done”。在等待期间,我们也睡眠了 10 秒钟,以便观察内存使用情况。

如果你运行该示例,你会发现程序会占用大量的内存,因为我们在协程中创建了一个非常大的切片。如果你查看系统监视器,你会看到 Go 运行时占用了几个 GB 的内存。这个示例可以让你更好地理解在协程中添加大的切片可能会导致的内存问题。

此外,如果您的协程需要频繁地修改切片,那么使用大型切片会导致分配和释放内存的开销增加,从而降低性能。

因此,在协程中使用大型切片时,最好使用合适的数据结构和算法来减少内存消耗,并尽可能避免频繁地修改切片。

另外,在协程中使用切片,可以考虑使用通道(channel)来传递数据。通道可以帮助您避免并发访问切片的问题,并且可以更好地控制内存使用。您可以在主线程中创建一个切片,然后将其传递给协程通过通道进行读写。

package main
​
import (
    "fmt"
    "sync"
)
​
//使用slice切片指针,减少内存的复制
func main() {
    var wg sync.WaitGroup
    slice := make([]int, 100000000)
​
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(s *[]int) {
            defer wg.Done()
​
            // 在协程中使用指针来操作切片
            for i := range *s {
                (*s)[i] = i
            }
        }(&slice)
    }
​
    wg.Wait()
    fmt.Println(slice[0])
}

除了使用通道和对象池等技术外,还有一些其他的最佳实践可以帮助您在协程中使用切片:

  1. 在创建切片时,可以通过预分配来减少内存分配的次数。如果您已经知道切片的大小,请在创建切片时指定它的容量。这将使切片的底层数组分配足够的内存,避免在添加新元素时不断分配新的内存。
  2. 在协程中使用切片时,避免对切片进行修改操作。如果必须对切片进行修改操作,请确保使用适当的同步机制来保护对切片的并发访问。
  3. 在协程中使用切片时,避免使用共享状态。如果协程之间需要共享数据,请使用通道或其他并发安全的数据结构来确保数据一致性。
  4. 在协程中使用切片时,可以通过限制协程的数量来避免资源竞争和内存泄漏。如果您需要启动大量的协程来处理切片,可以使用 worker 模式,将任务分配给固定数量的协程,从而避免协程数量过多造成的问题。

总之,在协程中使用切片需要小心谨慎,并且需要了解并发编程的最佳实践。通过遵循这些最佳实践,您可以确保协程能够正确地使用切片,并且可以最大限度地发挥其优势,提高应用程序的性能和可靠性。

切片和协程的常见问题和解决方案:

  1. 如何在协程中安全地修改切片?

在协程中修改切片需要考虑并发安全性问题,因为多个协程可能同时访问和修改同一个切片。为了避免竞争条件和数据竞争,可以使用 Go 语言提供的 sync 包中的锁来保护对切片的访问。例如,可以使用 sync.Mutex 或 sync.RWMutex 来保护对切片的读写操作。

另外,还可以使用通道来安全地传递和共享数据。例如,可以使用无缓冲通道来传递切片,并使用 select 语句来确保协程在安全的条件下修改切片。

  1. 如何在协程中安全地遍历切片?

在协程中遍历切片也需要考虑并发安全性问题,因为在遍历过程中可能会有其他协程修改切片的内容。为了避免这种情况,可以使用 sync 包中的锁来保护对切片的访问。另外,可以使用 Go 语言提供的 range 关键字来遍历切片,并使用通道来传递遍历结果,从而实现并发安全性。

  1. 如何在协程中传递大型切片?

在协程中传递大型切片时,最好使用指向切片的指针,而不是复制整个切片。这样可以避免大量的内存复制和垃圾回收,提高程序的性能和效率。同时,为了避免竞争条件和数据竞争,最好使用通道来传递指向切片的指针。

  1. 如何在协程中删除切片中的元素?

在协程中删除切片中的元素需要考虑并发安全性问题,因为删除元素可能会改变切片的长度和容量,从而影响其他协程对切片的访问。为了避免竞争条件和数据竞争,可以使用 sync 包中的锁来保护对切片的访问。另外,可以使用 Go 语言提供的 copy 函数和切片的特性来删除元素,从而实现并发安全性。

总之,切片和协程是 Go 语言并发编程中非常重要的组成部分,它们可以帮助我们编写高效、可维护和并发安全的程序。通过遵循最佳实践和了解 Go 语言的并发模型,我们可以最大化地发挥它们的优势。

其他的技巧和工具可以帮助您在协程中使用切片:

  1. 使用 sync.Pool 来缓存切片。sync.Pool 是 Go 语言提供的一个对象池,可以用于缓存和重用对象,从而减少内存分配和垃圾回收的开销。您可以使用 sync.Pool 来缓存和重用切片,从而避免不必要的内存分配。
  2. 使用 go vet 工具来检查切片的并发使用。go vet 是 Go 语言提供的一个静态分析工具,可以用于检查代码中的常见错误和不良实践。您可以使用 go vet 工具来检查协程中使用的切片是否存在并发安全性问题和数据竞争。
  3. 使用 race detector 工具来检测数据竞争。race detector 是 Go 语言提供的一个工具,可以用于检测并发程序中的数据竞争。您可以使用 race detector 工具来检测协程中使用的切片是否存在数据竞争问题。
  4. 使用工具来分析和优化内存使用。Go 语言提供了多个工具,如 pprof 和 trace,可以用于分析和优化程序的内存使用。您可以使用这些工具来了解程序中的内存使用情况,找出内存泄漏和资源竞争等问题,并进行优化。

总之,在协程中使用切片需要仔细考虑并发安全性和内存消耗等问题,同时可以使用一些工具和技巧来帮助分析和优化程序的性能和可靠性。