GO语言基础篇(二十三)- Goroutine&channel实践

404 阅读1分钟

这是我参与8月更文挑战的第 23 天,活动详情查看: 8月更文挑战

Goroutine&channel实践

本文分享两个例子来帮助理解goroutine和channel的联合使用

使用channel来等待goroutine结束

还是以上一篇文章的示例为例(点这里查看),我们对它进行改进,源代码如下:

package main

import (
    "fmt"
)

func doWorker(id int, c chan int) {
    for n := range c {
        fmt.Printf("Worker %d received %c\n", id, n)
    }
}

func createWorker(id int) chan<- int {
    c := make(chan int)
    go doWorker(id,c)
    return c
}

func chanDemo() {
    var channels [10]chan<- int
    for i := 0; i < 10; i++ {
        channels[i] = createWorker(i)
    }

    //向10个channel发送数据
    for i := 0; i < 10; i++ {
        channels[i] <- 'a' + i
    }

    for i := 0; i < 10; i++ {
        channels[i] <- 'A' + i
    }
}

func main() {
    chanDemo()
}

上边创建了10个channel,起了10个goroutine。向每个channel中都发送了一个大写字母和一个小写字母,并通过起的goroutine去打印通道中的数据

现在有一个问题就是,每个goroutine打印完成之后,如何通知外边,它打印完毕了

在上一篇文章中的结尾中,有留这么一句话:”不要通过共享内存进行通信;通过通信来共享内存“。所以我们先对上边的程序这样修改:

创建一个结构,里边有两个channel,一个用于发送数据(in),一个用于通知是够已接收完成(done)

package main

import (
    "fmt"
)

func doWorker(id int, c chan int, done chan bool) {
    for n := range c {
        fmt.Printf("Worker %d received %c\n", id, n)
        done <- true //打印完成之后,向对应的done通道中发送一个完成标记
    }
}

type worker struct {
    in chan int
    done chan bool
}

func createWorker(id int) worker {
    w := worker{
        in: make(chan int),
        done: make(chan bool),
    }
    go doWorker(id, w.in, w.done)
    return w
}

func chanDemo() {
    var workers [10]worker
    for i := 0; i < 10; i++ { // 创建10个worker
        workers[i] = createWorker(i)
    }

    //向10个channel发送数据
    for i := 0; i < 10; i++ {
        workers[i].in <- 'a' + i
        <-workers[i].done //每次发送完之后,从对应的done通道中取值
    }

    for i := 0; i < 10; i++ {
        workers[i].in <- 'A' + i
        <-workers[i].done
    }
}

func main() {
    chanDemo()
}

打印上边的结果会发现,结果是a、b、c、d、e、f.....这样顺序打印出来的。如果是顺序打印,这就没意义了,那就没必要建10个worker了,一个一个打印就完事儿了。我们是不希望往通道中发一个数据,就在那等它结束,而是希望一口气将20个数据发送出去,然后再一次性处理完成的标记

因此,只需要去掉两个发送数据的for循环中的<-workers[i].done语句,然后再循环处理workers中的done。因此对chanDemo函数做如下修改:

func chanDemo() {
    var workers [10]worker
    for i := 0; i < 10; i++ {
        workers[i] = createWorker(i)
    }

    //向10个channel发送数据
    for i, worker := range workers{
        worker.in <- 'a' + i
    }

    for i, worker := range workers {
        worker.in <- 'A' + i
    }

    //wait for all of them
    for _, worker := range workers {//因为每个worker发了两次数据,所以每个worker需要收两遍done
        <-worker.done
        <-worker.done
    }
}

然后执行的时候发现,小写字母都打印出来了,但是打印大写字母的时候就报阻塞了。这个原因是,第一个发送数据的循环往worker.in中发送数据之后,goroutine接收打印的时候,会往每个worker.done中发送数据true,但是并没有接收操作,所以再循环往worker中发送数据,执行worker函数的时候,因为每个woker.done已经都阻塞在那了,所以就报错了

一个不太规范的处理方法就是,在往worker.done中发送true的操作,单独开goroutine去处理

go func() {done <- true}()

对于等待多人来完成任务这件事情,go语言的库提供了帮助方法:waitGroup

waitGroup的使用

下边就通过waitGropu来对原来的程序进行修改

package main

import (
    "fmt"
    "sync"
)

func doWorker(id int, c chan int, wg *sync.WaitGroup) {
    for n := range c {
        fmt.Printf("Worker %d received %c\n", id, n)
        wg.Done()
    }
}

type worker struct {
    in chan int
    wg *sync.WaitGroup
}

func createWorker(id int, wg *sync.WaitGroup) worker {
    w := worker{
        in: make(chan int),
        wg: wg,
    }
    go doWorker(id, w.in, wg)
    return w
}

func chanDemo() {
    var wg sync.WaitGroup //新生成一个WaitGroup

    var workers [10]worker
    for i := 0; i < 10; i++ {
        workers[i] = createWorker(i, &wg)
    }

    wg.Add(20)//加多少个任务(因为我们这知道是有20个)

    //向10个channel发送数据
    for i, worker := range workers{
        worker.in <- 'a' + i
        //每次在这wg.Add(1)也行
    }

    for i, worker := range workers {
        worker.in <- 'A' + i
	//每次在这wg.Add(1)也行
    }

    wg.Wait()//等待20个任务全部做完
}

func main() {
    chanDemo()
}

可以看到执行结果中,大小写字母是混合打印出来的。说明上边是并行打印的

上边就展示了使用go自带的waitGroup来做等待多人的事情

使用channel来实现树的遍历

在函数式编程那篇文章中分享了一个通过传递函数的方式来实现树的遍历及节点数的统计(不清楚通过函数式编程的方式实现树的遍历及统计的,可以点这里),下边是源代码

tree/node.go

package tree

import "fmt"

type Node struct {
    Value int
    Left, Right *Node
}

func (n *Node) Print() {
	fmt.Println(n.Value)
}

func (n *Node) Traverse() { // 中序遍历

    n.TraverseFunc(func(node *Node) {
        fmt.Println(node.Value)
    })

    fmt.Println()
}

func (n *Node) TraverseFunc(f func(node *Node)) {
    if n == nil {
        return
    }
    n.Left.TraverseFunc(f)
    f(n)
    n.Right.TraverseFunc(f)
}
=================================

entry/entry.go

package main

import (
    "fmt"
    "google.go/part6/functional/closure/tree"
)

func main() {
    var root tree.Node
    root = tree.Node{Value: 1}
    root.Left = &tree.Node{Value: 2}
    root.Right = &tree.Node{Value: 3}
    root.Left.Right = &tree.Node{Value: 4}
    root.Right.Left = &tree.Node{Value: 5}
    root.Traverse()

    //如果想计算节点的数量,就可以这么做
    nodeCount := 0
    root.TraverseFunc(func(node *tree.Node) {
        nodeCount++
    })
    fmt.Println("Node Count: ", nodeCount)
}

下面在这个基础上,再增加上一种通过channel的方式,找出树中最大的节点,如下:

tree/node.go
func (node *Node) TraversWithChannel() chan *Node {
    out := make(chan *Node) //创建一个channel,类型是*Node
    go func() {
        node.TraverseFunc(func(node *Node) {
            out<-node
        })
        close(out)
    }()

    return out
}

=================================
entry/entry.go

c := root.TraversWithChannel() // 每次获取到一个channel
    maxNode := 0
    for node := range c { //逐个获取channel中的节点(此时就可以做计数、遍历、统计等)
            if node.Value > maxNode {
            maxNode = node.Value
        }
    }

    fmt.Println("Max node Value:", maxNode)

以上就是简单的使用一下goroutine和channel,比较浅显。作为go的小白,对goroutine的执行顺序以及channel的接收、关闭之类的,还是比较懵,特别是go语言的不同版本对goroutine的支持发生了变化(1.14及之前的版本),还是需要深入的去学习一下goroutine的内部调度原理,才能使用goroutine的时候得心应手。以上内容,如有不正确的地方,欢迎指正