goroutine

约 1950 字大约 7 分钟

以下引于 claude.ai

goroutine是Go语言中的轻量级线程。具体来说,goroutine有以下几个主要特征:

  1. goroutine由Go运行时(runtime)管理,可以实现高度的并发。一个程序可以同时运行成千上万个goroutine。
  2. goroutine之间通过channel进行通信。channel使得不同goroutine之间可以安全地共享内存。
  3. 创建goroutine的代价非常小。因为goroutine的堆栈起始很小(仅2KB),所以可以很容易地创建很多goroutine。
  4. goroutine使用协作式调度(cooperative scheduling),多个goroutine可以在一个或几个操作系统线程上执行。Go运行时内部使用自举调度器在goroutine之间快速切换。
  5. goroutine通信安全。通信和内存同步都隐式实现,不需加锁。 总之,goroutine作为Go并发的核心,使得编写并发程序变得简单、高效。它独特的设计对Go语言的高并发性能起到关键作用。

创建goroutine

go ,只要在函数前添加它,就实现了并发。

package main

import (
	"fmt"
	"time"
)

func SubTask() {
	for {
		fmt.Println(time.Now().Format("15:04:05"), "this is a SubTask")
		time.Sleep(5 * 100 * time.Millisecond) // 延迟0.5s
	}
}
func main() {
	go SubTask() //创建一个协程

	time.Sleep(time.Second) // 延迟1s

	fmt.Println(time.Now().Format("15:04:05"), "this is a main goroutine")
}

这里就可以到一个问题,虽然SubTask是个循环,但是main执行完毕的时候,它也就结束了。所以这里需要注意,当主协程结束的时候,子协程也会退出。那我们做并行的时候,一定要注意这种关系

如何保证子协程执行完毕,主协程再退出呢?

runtime

Gosched

以下引于 claude.ai

gosched()是一个Go运行时(runtime)提供的函数,它允许当前goroutine主动放弃CPU,将CPU时间片让给其他可运行的goroutine。

具体来说,gosched()函数有以下几个主要作用:

  1. 将CPU时间片让给同一物理线程上的其他goroutine运行。因为Go运行时会对goroutine进行合理调度,所以一个线程上可能有多个goroutine在运行。gosched()可以在适当的时候放弃时间片,避免goroutine独占CPU。
  2. 当缺少goroutine可运行时,它允许运行时内核线程安全地寻找其他物理线程上的可运行goroutine。如果找到,可以提前恢复已阻塞的goroutine,并为其分配CPU时间。
  3. 在多核环境下,它也可以触发work-stealing算法,让处于空闲的线程帮助正在执行的线程,提高多核CPU的利用率。
  4. 最后,gosched()还可以作为runtime内部算法的一部分,帮助对goroutine、线程进行优化管理。 所以,gosched()对Go并发程序的性能优化是非常重要的。使用它可以达到更高的CPU利用率,最小化线程阻塞,优化goroutine执行。
package main

import (
	"fmt"
)

func main() {
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Println("goroutine...")
		}
	}()

	for i := 0; i < 2; i++ {
		//runtime.Gosched()
		fmt.Println("hello main")
	}

}

子协程要么没执行,要么顺序是乱的

无Gosched
无Gosched

按预期执行

Gosched
Gosched

Goexit

以下引于 claude.ai

goexit是Go语言runtime包中提供的一个函数,它可以立即终止当前goroutine的执行,但是defer函数会正常执行。

具体来说,goexit函数有以下几个重要作用:

  1. 立即终止当前goroutine,抛弃当前goroutine所占用的资源。
  2. 正常执行defer指定的函数。defer函数会按照逆序执行并退出,资源正常释放。
  3. 直接终止goroutine,不会造成阻塞。如果只调用return,仍需等待函数返回。
  4. 终止隐式创建的goroutine。一些操作如go语句、通道操作会隐式创建goroutine。goexit可以安全终止它们。
  5. 在一些阻塞的场景下,可以使用goexit完全终止goroutine,如死锁场景。
  6. 终止goroutine时,退出执行而不影响程序其它goroutine的执行。 总之,goexit可以强制但优雅地终止当前goroutine。它通常在goroutine中使用,特别是在出现致命错误需要立即退出,或者隐式goroutine需要立即停止的场景下使用。
package main

import (
	"fmt"
	"runtime"
)

func test() {
	defer fmt.Println("333")
	runtime.Goexit() //表示终止所在的协程

	fmt.Println("444")
}

func main() {
	//创建新的协程
	go func() {
		fmt.Println("111")
		//调用别的函数
		test()
		fmt.Println("222")
	}()
	for {
	}
}

使用Goexit
使用Goexit
不使用Goexit
不使用Goexit

GOMAXPROCS

以下引于 claude.ai

GOMAXPROCS是Go语言中的一个重要环境变量,它控制Go程序可以同时使用的CPU逻辑核心数。

具体来说,GOMAXPROCS主要有以下几个作用:

  1. 它设置可同时执行Go代码的OS线程数。默认值是机器上的CPU核心数。

  2. 通过将其设置为1,可以限制Go程序只使用1个OS线程,用于调试。

  3. 在多核环境下,适当增加GOMAXPROCS可以使Go程序利用多核心,提高并发性能。比如设置为CPU核心数的50%~80%。

  4. GOMAXPROCS也影响Go的并行GC和调度器,增加GOMAXPROCS需要相应调整运行时参数。

  5. 在容器、serverless等环境下,合理设置GOMAXPROCS可以限制Go程序占用的CPU资源。

总之,GOMAXPROCS能够合理限制和利用Go程序的并发性。但设置不当也会适得其反,需要根据具体场景选择合适的值。适当增加GOMAXPROCS可以提高CPU利用率,但超过物理核心数效果不大。

package main

import (
	"fmt"
	"runtime"
)

func main() {
	n := runtime.GOMAXPROCS(1) //指定以单核运算
	fmt.Println("n = ", n)
	for {
		go fmt.Print(1)
		fmt.Print(0)
	}
}

单核最多只有一个goroutine,所以就是000...111...000这样的循环打印。多核就是交替打印

单核
单核
多核
多核

sync.WaitGroup

以下引于 claude.ai

sync.WaitGroup是Go语言中的一个同步原语,它可以用于等待一组goroutine都完成执行。

具体来说,sync.WaitGroup有以下几个主要用法:

  1. 在开始执行一组goroutine时,调用Add添加计数。

  2. 每个goroutine执行完成后,调用Done来减少计数。

  3. 主goroutine调用Wait方法来等待计数减为0,则表示所有goroutine都执行完成。

  4. 可以使用counters来获取当前的计数值。

  5. WaitGroup重用需要调用Reset方法重置计数值为0。

  6. WaitGroup使用一个计数器和cond来实现阻塞等待,以优雅地同步多个goroutine。

  7. WaitGroup可用于生产者-消费者队列,确保完全处理完成。

  8. 也可以用来等待直到关闭通道,等待完成操作的超时等场景。

总之,WaitGroup是一个非常有用的同步工具,可以方便协调goroutine,实现阻塞直到指定的一组goroutine都结束执行。

//UpLoadFile 将整个目录都上传,遍历所有文件夹及子文件夹上传
func (c *MinioClient) UpLoadFile() {
	bucketName := os.Getenv("MINIO_BUCKET_NAME") //加判断
	if bucketName == "" {
		panic("MINIO_BUCKET_NAME is empty")
	}
	var files []string
	// 下载路径和bucket路径保持一致
	err := filepath.Walk(bucketName, func(path string, info os.FileInfo, err error) error {
		if !info.IsDir() {
			files = append(files, path)
		}
		return nil
	})
	if err != nil {
		panic(err)
	}

	var wg sync.WaitGroup
	wg.Add(len(files))

	log.Println("请稍等,执行中....")

	for k, file := range files {
		fileID := k

		go func(filePath string) {
			defer wg.Done()
			objectName := strings.Replace(filePath, bucketName, "", -1)

			// 检查对象是否存在
			objectInfo, _ := c.Conn.StatObject(context.Background(), bucketName, objectName, minio.GetObjectOptions{})

			if objectInfo.Key == "" {
				log.Println(fileID, filePath, objectName)
				_, err := c.Conn.FPutObject(context.Background(), bucketName, objectName, filePath, minio.PutObjectOptions{
					ContentType: "",
				})
				if err != nil {
					panic(err)
				}
			}
		}(file)
	}
	wg.Wait()
	log.Println("Done")
	return
}

这个是写的Minio的初始化工具,用来实现文件类的离线部署和自动化交付,中间就使用了WaitGroup,减少文件导入的时间