多种阻塞进程方法的消耗比较

工作中遇到一个需要阻塞系统以启动消费者的问题,谷歌到多种阻塞方法,但均未提及资源(cpu)消耗。所以特将多种方法的消耗进行实验,以期得到一个资源消耗的基本结论。

阻塞系统的各种方法及其消耗

使用waitgroup阻塞

  1. 代码
func main() {
	wg := sync.WaitGroup{}
	go func() {
		wg.Add(1)
		defer wg.Done()
		pid := os.Getpid()
		fmt.Printf("子协程 PID: %d \n", pid)
		time.Sleep(120 * time.Second)
	}()
	pid := os.Getpid()
	fmt.Printf("主协程 PID: %d \n", pid)
	wg.Add(1)
	wg.Wait()
}
  1. 输出
leetcode go build -o leet leetcode.go                                                503ms
➜  leetcode ./leet                                                                   266ms
主协程 PID: 78932
子协程 PID: 78932
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000078f70)
        /Users/brick/sdk/go1.17.1/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0x10c1860)
        /Users/brick/sdk/go1.17.1/src/sync/waitgroup.go:130 +0x71
main.main()
        /Users/brick/personalspace/golearn/leetcode/leetcode.go:52 +0xd7
  1. 资源消耗
➜  ~ procs leet                                                                                154ms
 PID:▲ User  │ TTY  CPU MEM CPU Time │ Command
             │      [%] [%]78932 brick │ s001 0.0 0.0 00:00:00 │ ./leet

使用empty select阻塞

  1. 代码
func main() {
	go func() {
		pid := os.Getpid()
		fmt.Printf("子协程 PID: %d \n", pid)
		time.Sleep(120 * time.Second)
	}()
	pid := os.Getpid()
	fmt.Printf("主协程 PID: %d \n", pid)
	select {}
}
  1. 输出
➜  leetcode ./leet                                                              8ms
主协程 PID: 83310
子协程 PID: 83310
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
        /Users/brick/personalspace/golearn/leetcode/leetcode.go:47 +0x79
➜  leetcode
  1. 资源消耗
➜  ~ procs leet                                                                        165ms
 PID:▲ User  │ TTY  CPU MEM CPU Time │ Command
             │      [%] [%]83310 brick │ s001 0.0 0.0 00:00:00 │ ./leet
➜  ~

使用死循环

  1. 代码
func main() {
	pid := os.Getpid()
	fmt.Printf("主协程 PID: %d \n", pid)
	for {
	}
}
  1. 输出
➜  leetcode ./leet                                                                         242ms
主协程 PID: 84197
^C
➜  leetcode
  1. 资源消耗
➜  ~ procs leet                                                               168ms
 PID:▲ User  │ TTY    CPU MEM CPU Time │ Command
             │        [%] [%]84197 brick │ s001 105.7 0.0 00:00:10 │ ./leet
➜  ~ procs leet                                                               143ms
 PID:▲ User  │ TTY    CPU MEM CPU Time │ Command
             │        [%] [%]84197 brick │ s001 100.0 0.0 00:00:12 │ ./leet
➜  ~ procs leet                                                               142ms
 PID:▲ User  │ TTY   CPU MEM CPU Time │ Command
             │       [%] [%]84197 brick │ s001 99.0 0.0 00:00:13 │ ./leet
➜  ~ procs leet                                                               139ms
 PID:▲ User  │ TTY   CPU MEM CPU Time │ Command
             │       [%] [%]84197 brick │ s001 96.3 0.0 00:00:14 │ ./leet
➜  ~ procs leet                                                               141ms
 PID:▲ User  │ TTY   CPU MEM CPU Time │ Command
             │       [%] [%]84197 brick │ s001 99.1 0.0 00:00:15 │ ./leet
➜  ~

使用mutex

  1. 代码
func main() {
	var m sync.Mutex
	go func() {
		pid := os.Getpid()
		fmt.Printf("子协程 PID: %d \n", pid)
		time.Sleep(60 * time.Second)
	}()
	pid := os.Getpid()
	fmt.Printf("主协程 PID: %d \n", pid)
	m.Lock()
	m.Lock()
}
  1. 输出
➜  leetcode ./leet                                                         245ms
主协程 PID: 86143
子协程 PID: 86143
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0x13, 0x60, 0xc000072f10)
        /Users/brick/sdk/go1.17.1/src/runtime/sema.go:71 +0x25
sync.(*Mutex).lockSlow(0xc0000160a8)
        /Users/brick/sdk/go1.17.1/src/sync/mutex.go:138 +0x165
sync.(*Mutex).Lock(...)
        /Users/brick/sdk/go1.17.1/src/sync/mutex.go:81
main.main()
        /Users/brick/personalspace/golearn/leetcode/leetcode.go:50 +0xcc
➜  leetcode
  1. 资源消耗
➜  ~ procs leet                                                             151ms
 PID:▲ User  │ TTY  CPU MEM CPU Time │ Command
             │      [%] [%]86143 brick │ s001 0.0 0.0 00:00:00 │ ./leet
➜  ~

使用chan

  1. 代码
func main() {
	c := make(chan struct{})
	go func() {
		pid := os.Getpid()
		fmt.Printf("子协程 PID: %d \n", pid)
		time.Sleep(60 * time.Second)
	}()
	pid := os.Getpid()
	fmt.Printf("主协程 PID: %d \n", pid)
	<-c
}
  1. 输出
➜  leetcode go build -o leet leetcode.go                                      139ms
➜  leetcode ./leet                                                                                                          249ms
主协程 PID: 87117
子协程 PID: 87117
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /Users/brick/personalspace/golearn/leetcode/leetcode.go:48 +0x95
➜  leetcode
  1. 资源消耗
➜  ~ procs leet                                                              152ms
 PID:▲ User  │ TTY  CPU MEM CPU Time │ Command
             │      [%] [%]87117 brick │ s001 0.0 0.0 00:00:00 │ ./leet
➜  ~

使用signal

  1. 代码
func main() {
	sig := make(chan os.Signal, 2)
	signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
	<-sig
}
  1. 输出
➜  leetcode go build -o leet leetcode.go                                              542ms
➜  leetcode ./leet                                                                    261ms

  1. 资源消耗
➜  ~ procs leet                                                                       149ms
 PID:▲ User  │ TTY  CPU MEM CPU Time │ Command
             │      [%] [%]89015 brick │ s001 0.0 0.0 00:00:00 │ ./leet
➜  ~

结果分析

对比上述各种方法,可发现除使用for循环阻塞系统,cpu的消耗率可达到100%外,其余阻塞方法对cpu的消耗均接近于0。可见for是最不经济和最不可取的阻塞方法。其余方法也有局限性,即程序在所有groutine都阻塞后,会报错all goroutines are asleep - deadlock!,即需要额外代码维护程序的正常运行。

使用signal进行阻塞,本质上也是利用chan在进行阻塞,但不需要额外的操作以避免goroutine死锁的情况。只针对阻塞系统的目标而言,推荐使用signal实现。

参考

  1. 在 Golang 中各种永远阻塞的姿势
  2. DIFFERENT WAYS TO BLOCK GO RUNTIME FOREVER