【并发编程】控制两个Goroutine交替打印

交替打印

问题描述:使用两个 goroutine 交替打印序列,一个 goroutine 打印数字,另外一个 goroutine 打印字母,最终效果如下:

12AB34CD56EF78GH910IJ1112KL1314MN1516OP1718QR1920ST2122UV2324WX2526YZ2728

这里主要考察线程间的通信方式,在 Go 语言中可使用 channel 传递消息来控制打印的进度,或使用其他特殊方式实现(如转成单线程程序)。

方法一:3个 channel

可以使用三个 channel 来实现,其中:

  • 1 个 letterChan 通知打印字母的 goroutine
  • 1 个 numberChan 通知打印数字的 goroutine
  • 1 done channel 接受终止信号
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func main() {
	// 3 个 channel
	numberChan := make(chan bool) // letterChan 通知打印字母的 goroutine
	letterChan := make(chan bool) // numberChan 通知打印数字的 goroutine
	done := make(chan bool)       // done 接受终止信号

	// print number
	go func() {
		x := 1
		for {
			select {
			case <-numberChan:
				fmt.Print(x)
				x++
				fmt.Print(x)
				x++
				letterChan <- true
			}
		}
	}()

	// print letter
	go func() {
		c := 'A'
		for {
			select {
			case <-letterChan:
				// 判断停止
				if c > 'Z' {
					done <- true
					return
				}
				fmt.Print(string(c))
				c++
				fmt.Print(string(c))
				c++
				numberChan <- true
			}
		}
	}()

	// 向一端发信号 开始打印
	numberChan <- true
	// letterChan <- true

	// 主线程阻塞等待
	for {
		select {
		case <-done:
			return
		}
	}
}

方法二:2个 channel+wait

控制 goroutine 结束除了使用 channel 还可以使用 sync.wait 来实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
func main() {
	// 两个 channel 负责通知
	numberChan := make(chan bool) // letterChan 通知打印字母的 goroutine
	letterChan := make(chan bool) // numberChan 通知打印数字的 goroutine

	// wait 用来等待字母打印完成后退出循环
	wait := sync.WaitGroup{}

	// print number
	go func() {
		x := 1
		for {
			select {
			case <-numberChan:
				fmt.Print(x)
				x++
				fmt.Print(x)
				x++
				letterChan <- true
			}
		}
	}()

	wait.Add(1)

	// print letter
	go func() {
		c := 'A'
		for {
			select {
			case <-letterChan:
				// 判断停止
				if c > 'Z' {
					wait.Done()
					return
				}
				fmt.Print(string(c))
				c++
				fmt.Print(string(c))
				c++
				numberChan <- true
			}
		}
	}()

	// 向一端发信号 开始打印
	numberChan <- true
	// letterChan <- true

	// 主线程阻塞等待
	wait.Wait()
}

方法三:Gosched

通过 runtime.GOMAXPROCS(1) 设置 P 的数量为 1,保证只有一个线程执行任务。每次打印完主动调用 runtime.Gosched() 切换 goroutine 执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func main() {
	runtime.GOMAXPROCS(1)

	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		c := 'A'
		for c <= 'Z' {
			fmt.Print(string(c))
			c++
			fmt.Print(string(c))
			c++
			runtime.Gosched() // 挂起当前 goroutine
		}
	}()

	go func() {
		defer wg.Done()
		i := 0
		for i <= 26 {
			fmt.Print(i)
			i++
			fmt.Print(i)
			i++
			runtime.Gosched() // 挂起当前 goroutine
		}
	}()

	wg.Wait()
}

注意点

  • 方法一和二都只退出了 letter goroutinenumber goroutine 在 main 函数结束后自动结束
  • 方法三将程序变成了单线程,runtime.Gosched() 可使用 time.Sleep(100 * time.Microsecond) 替代,但 sleep 的时间不易控制,不推荐。
  • 方法一和二通过选择初始信号发给哪个 channel 来控制打印的顺序,方法三实测可以改变 go func() 代码顺序来切换(不确定是否一定正确,原理还未理清,最好想一个更优雅的切换方式)

Reference

0%