收藏JavaJava 程序员

面试官:goroutine和channel 如何控制并发顺序?

2022-04-10  本文已影响0人  马小莫QAQ

最近有同学问我这个问题。

题目意思是 利用goroutine和channel 连续输出10次,dog,cat,fish,并且都要按照这个dog,cat,fish的顺序输出。

分析

题目既然要求是使用goroutine,那么我们肯定是要控制好这个并发的顺序。因为并发是具有随机性的,这个题目并不难,很典型的chan控制进程之间的顺序。

那我们先了解一下 goroutine ,select ,sync.WaitGroup,channel

1. goroutine

我们这里先了解一下go的调度机制,即是GPM模型。goruntine相对线程更加轻量,GPM调度器效率更高。

每个Processor对象都拥有一个LRQ(Local Run Queue),未分配的Goroutine对象保存在GRQ(Global Run Queue )中,等待分配给某一个P的LRQ中,每个LRQ里面包含若干个用户创建的Goroutine对象。

同时Processor作为桥梁对Machine和Goroutine进行了解耦,也就是说Goroutine如果想要使用Machine需要绑定一个Processor才行。

2. select

select 和 switch 很像,它不需要输入参数,并且仅仅被使用在通道操作上

select 语句被用来执行多个通道操作的一个和其附带的 case 块代码。

我们知道 select 语句和 switch 很像,不同点是用通道读写操作代替了布尔操作。 通道将被阻塞,除非它有默认的 default 块 (之后将介绍)。一旦某个 case 条件执行,它将不阻塞。 我们发现 select 语句将阻塞,因此 select 将等待,直到有 case 语句不阻塞。

可以使用 select 模拟了一个数百万请求的服务器负载均衡的例子,它从多个有效服务中返回其中一个响应。

使用协程,通道和 select 语句,我们可以向多个服务器请求数据并获取其中最快响应的那个。

3. sync.WaitGroup

WaitGroup 是一个带着计数器的结构体,这个计数器可以追踪到有多少协程创建,有多少协程完成了其工作。当计数器为 0 的时候说明所有协程都完成了其工作。

4. channel

channel 是 goroutine 之间通信的一种方式,可以类比成 Unix 中的进程的通信方式管道。

channel 提供了一种通信机制,通过它,一个 goroutine 可以想另一 goroutine 发送消息。channel 本身还需关联了一个类型,也就是 channel 可以发送数据的类型。例如: 发送 int 类型消息的 channel 写作 chan int 。

5. 代码

简单了解完上述之后,我们开始写代码。

解释

既然是并发,那么我们就要写3个函数,去分别打印我们的dog,cat,fish了。

这里用dog进行举例

func dog(){
    fmt.Println("dog")
}

那我们的主函数就要启动goroutine去并发了。大概就是一下这种情况。

func main(){
    //...省略一些逻辑
    go dog()
    go cat()
    go fish()
    //...省略一些逻辑
}

那么我们先控制这三个的并发顺序,可以直接select去阻塞进行调试。

既然要控制并发顺序,我们就要可以用channel进行通信通知。我们先创建三个channel,用chan去传递信息。注意这里是传递无缓冲的channel,因为无缓冲是可以进行读写同步的。用来控制并发顺序最合适不过了。

dogChan, catChan, fishChan := make(chan bool), make(chan bool), make(chan bool)

dogChan 一开始赋值,并且dog打印完之后,给catChan通信,cat打印完之后,给fishChan通信,fish打印完后给dogChan通信。打完10次之后就停止。

比如这个传入dogChan 和 catChan 进行通信。把dogChan的取出,再将catChan的赋值,就可以不断进行循环调度了。

func dog(dogChan chan bool,catChan chan bool ) {
    for {
        select {
        case <-dogChan:
            fmt.Println("dog")
            catChan <- true
            break
        default:
            break
        }
    }
}

我们主程序可以用 sync.WaitGroup 来进行阻塞。当完成10次之后才Done掉,那么就完成了。

func fish(fishChan chan bool,dogChan chan bool ) {
    i := 0
    for {
        select {
        case <-fishChan:
            fmt.Println("fish")
            i++
            if i > 9 {
                wg.Done()
                return
            }
            dogChan <- true
            break
        default:
            break
        }
    }
}

完整代码

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func dog(dogChan chan bool,catChan chan bool ) {
    for {
        select {
        case <-dogChan:
            fmt.Println("dog")
            catChan <- true
            break
        default:
            break
        }
    }
}

func cat(catChan chan bool,fishChan chan bool ) {
    for {
        select {
        case <-catChan:
            fmt.Println("cat")
            fishChan <- true
            break
        default:
            break
        }
    }
}

func fish(fishChan chan bool,dogChan chan bool ) {
    i := 0
    for {
        select {
        case <-fishChan:
            fmt.Println("fish")
            i++ // 计数,打印完之后就溜溜结束了。
            if i > 9 {
                wg.Done()
                return
            }
            dogChan <- true
            break
        default:
            break
        }
    }
}

func main() {
    dogChan, catChan, fishChan := make(chan bool), make(chan bool), make(chan bool)
    wg.Add(1)
    go dog(dogChan, catChan)
    go cat(catChan, fishChan)
    go fish(fishChan, dogChan)
    dogChan <- true // 记得这里进行启动条件,不然就没法启动了。
    wg.Wait()
}

作者:小生凡一
链接:https://juejin.cn/post/7084210766173667359
来源:稀土掘金

上一篇 下一篇

猜你喜欢

热点阅读