多线程实现交替打印
多线程实现交替打印
首先提出该问题的面试官主要是考察,你对协程之间相互通信和协程的生命周期管理的考察,所以我们首先想到的是用chan
进行协程之间通信,用sync.WaitGroup
来管理协程的生命周期
两个协程交替打印
错误解法:
func main() {
var ch1 = make(chan int)
var ch2 = make(chan int)
var ch3 = make(chan bool)
go func() {
for a := 0; a < 5; a++ {
i := <-ch1
fmt.Println("goroutine 1: ", i)
ch2 <- i + 1
}
}()
go func() {
for a := 0; a < 5; a++ {
i := <-ch2
fmt.Println("goroutine 2: ", i)
ch1 <- i + 1
}
}()
ch1 <- 1
}
这种解法无法控制main协程在其他协程工作结束之后才结束,可能打印工作还没结束,主协程就结束了。
方案一:使用两个chan
来控制来控制两个协程的工作顺序,并使用用sync.WaitGroup
来管理协程的生命周期
package main
import (
"fmt"
"sync"
)
func main() {
var ch1 = make(chan int, 1)
var ch2 = make(chan int, 1)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for a := 0; a < 5; a++ {
i := <-ch1
fmt.Println("goroutine 1: ", i)
ch2 <- i + 1
}
}()
go func() {
defer wg.Done()
for a := 0; a < 5; a++ {
i := <-ch2
fmt.Println("goroutine 2: ", i)
ch1 <- i + 1
}
}()
ch1 <- 1
// 等待协程执行完毕,main必须在其他协程完成工作之后才能结束
wg.Wait()
}
方案二:使用两个chan
来控制两个工作协程的的打印顺序,另一chan
来管理主协程在最后结束生命。
package main
import (
"fmt"
)
func main() {
var ch1 = make(chan int)
var ch2 = make(chan int)
var ch3 = make(chan bool)
go func() {
for a := 0; a < 5; a++ {
i := <-ch1
fmt.Println("goroutine 1: ", i)
ch2 <- i + 1
}
ch3 <- true
}()
go func() {
for a := 0; a < 5; a++ {
i := <-ch2
fmt.Println("goroutine 2: ", i)
ch1 <- i + 1
}
ch3 <- true
}()
ch1 <- 1
<-ch3
}
扩展-N个协程交替打印
例子-使用三个协程交替打印
package main
import (
"fmt"
"sync"
)
func getWorker(wg *sync.WaitGroup, waitCh chan int, symbol int) chan int {
notify := make(chan int)
wg.Add(1)
go func(waitCh chan int) {
defer func() {
wg.Done()
}()
fmt.Printf("waitCh Address of p=%p ,i:%d \n", waitCh, symbol)
for d := range waitCh {
if d >= 30 {
break
}
fmt.Println("goroutine:", symbol, "print", d)
notify <- d + 1
}
close(notify)
fmt.Println("goroutine: finish", symbol)
}(waitCh)
return notify
}
func main() {
goruntineNum := 3
wg := new(sync.WaitGroup)
start := make(chan int)
// 使用lastCh保存start值用于后续计算,start后面还要用。
lastch := start
fmt.Printf("waitCh Address of start p=%p ,lastch p=%p \n", start, lastch)
for i := 0; i < goruntineNum; i++ {
lastch = getWorker(wg, lastch, i+1)
}
// for循环之后lastch就是最后一个协程返回的通道,我们需要将通道内的值发送到第一个协程的通道内,就是下面的for range的操作
start <- 1
// 当最后一个协程打印之后通知第一个协程的通道,该工作了
for v := range lastch {
start <- v
}
close(start)
wg.Wait()
}
流程,第一个for循环是创建工作协程,并且每创建一个协程都会返回一个通道用于启动下一个协程,并且该chan还负责向下一个协程通知需要打印的数字。但是这样的话最后一个协程返回的通道没有协程使用,所以我们将最后返回的通道中的值发送给start,也就是第一个协程使用的通道,这样当一个协程打印数字之后就会通知下一个协程去打印下一个数字,而最后一个协程打印之后就会通知第一个协程继续打印,这样循环打印,直到打印完毕。
知识点:
这里的for
语句也可以被称为带有range
子句的for
语句,有如下特征:
- 一、这样一条
for
语句会不断地尝试从intChan2
种取出元素值,即使intChan2
被关闭,它也会在取出所有剩余的元素值之后再结束执行。 - 二、当
intChan2
中没有元素值时,它会被阻塞在有for
关键字的那一行,直到有新的元素值可取。 - 三、假设
intChan2
的值为nil
,那么它会被永远阻塞在有for
关键字的那一行。
除此以外,通道还可以和select结合使用:
// 准备好几个通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
fmt.Println("No candidate case is selected!")
}
在使用select
语句的时候,我们首先需要注意下面几个事情。
- 如果像上述示例那样加入了默认分支,那么无论涉及通道操作的表达式是否有阻塞,
select
语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。 - 如果没有加入默认分支,那么一旦所有的
case
表达式都没有满足求值条件,那么select
语句就会被阻塞。直到至少有一个case
表达式满足条件为止。 - 还记得吗?我们可能会因为通道关闭了,而直接从通道接收到一个其元素类型的零值。所以,在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。这对于程序逻辑和程序性能都是有好处的。
select
语句只能对其中的每一个case
表达式各求值一次。所以,如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for
语句中嵌入select
语句的方式实现。但这时要注意,简单地在select
语句的分支中使用break
语句,只能结束当前的select
语句的执行,而并不会对外层的for
语句产生作用。这种错误的用法可能会让这个for
语句无休止地运行下去。
多线程实现交替打印
http://example.com/2023/11/05/多线程实现交替打印/