Golang之Channel的理解与应用

版权声明:博客仅作为博主的个人笔记,未经博主允许不得转载。 https://blog.csdn.net/qq_35976351/article/details/81908952

博客参考自:https://golangbot.com/buffered-channels-worker-pools/

基础应用

使用channel的阻塞性质作为延时函数。

package main

import (
    "fmt"
)

func hello(done chan bool) {
    fmt.Println("Hello world goroutine !")
    done <- true
}

func main() {
    done := make(chan bool)
    go hello(done)
    <-done       // 只有done被hello函数写入true时,才会继续运行
    fmt.Println("main function")
}
/*
程序输出:
Hello world goroutine !
main function
*/

多个goroutine并发操作实例,计算数据,以123为例子介绍计算规则:

squares = (1 * 1) + (2 * 2) + (3 * 3) 
cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) 
output = squares + cubes = 50

代码:

package main

import "fmt"

func caclSquare(number int, squerop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit
        number /= 10
    }
    squerop <- sum
}

func calcCubes(number int, cubeop chan int) {
    sum := 0
    for number != 0 {
        digit := number % 10
        sum += digit * digit * digit
        number /= 10
    }
    cubeop <- sum
}

func main() {
    number := 589
    sqrch := make(chan int)
    cubech := make(chan int)
    go caclSquare(number, sqrch)
    go calcCubes(number, cubech)
    squares, cubes := <-sqrch, <-cubech     //  在这里同步所有操作
    fmt.Println("Final output:", squares+cubes)
}
/*
输出结果:
Final output:1536
*/

注意channel传递的是指针,需要有同步的操作。

死锁的例子:

package main

func main() {
    ch := make(chan int)
    ch <- 5
}
/*
报错提示:
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /home/erick/Desktop/Book/Sort_Go/test.go:5 +0x50
exit status 2
*/

也就是说,一个channel必须数据有数据在里面然后才可以取数据,否则就是死锁!

关闭channel操作:

v, ok := <- ch

ok==false说明已经关闭了ch

代码实例:

package main

import "fmt"

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}

func main() {
    ch := make(chan int)
    go producer(ch)
    for {
        v, ok := <-ch
        if ok == false {
            break
        }
        fmt.Println("Received: ", v, ok)
    }
}
/*
输出结果:
Received:  0 true
Received:  1 true
Received:  2 true
Received:  3 true
Received:  4 true
Received:  5 true
Received:  6 true
Received:  7 true
Received:  8 true
Received:  9 true
*/

代码解释:

proceduer程序中,每次写入一个数据后,这个goroutine就会阻塞 ;但是主程序的for循环每次会从ch中读出一个数据,之后proceduer继续写入,直到调用close()函数。

使用range loop重写上述的实现过程:

package main

import "fmt"

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}

func main() {
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received: ", v)
    }
}

带有缓冲机制的channel

ch := make(chan type, capacity)

一个缓冲队列拥有capacity个channel

简单实例:

package main

import "fmt"

func producer(chnl chan int) {
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}

func main() {
    ch := make(chan string, 2)
    ch <- "A"
    ch <- "B"
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
/*
输出:
A
B
*/

另一个实例:

package main

import (
    "fmt"
    "time"
)

func write(ch chan int) {
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Println("successfully wrote", i, "to ch")
    }
    close(ch)
}

func main() {
    ch := make(chan int, 2)
    go write(ch)
    time.Sleep(2 * time.Second)
    for v := range ch {
        fmt.Println("read value", v, "from ch")
        time.Sleep(2 * time.Second)
    }
}
/*
输出结果:
successfully wrote 0 to ch
successfully wrote 1 to ch
read value 0 from ch
successfully wrote 2 to ch
successfully wrote 3 to ch
read value 1 from ch
read value 2 from ch
successfully wrote 4 to ch
read value 3 from ch
read value 4 from ch
*/

代码的理解类似于之前的那个,注意channel是一个队列的机制,即先进先出!

队列也会出现死锁,队列里没有数据却进行读取则产生死锁,和单个的那个类似,代码示例:

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    <-ch
    <-ch
    <-ch
}
/*
报错输出:
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/erick/Desktop/Book/Sort_Go/test.go:21 +0xb8
exit status 2
*/

channel的长度len和容量capacity数组的概念一样,在这里不在赘述;不同的是,capacity确定后就不会更改了。

Wait Group和Worker Pool

WaitGroup:可以视为一组等待执行的goroutine的集合。

代码实例:

package main

import (
    "sync"
    "fmt"
    "time"
)

func process(i int, wg *sync.WaitGroup) {   // 注意传递的是地址!
    fmt.Println("started goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("goroutine %d ended\n", i)
    wg.Done()  // 表示完成工作!
}

func main() {
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1)  // 工作个数增加一个 
        go process(i, &wg)   // 传入地址!
    }
    wg.Wait()
    fmt.Println("All goroutines finished executing")
}

sync.WaitGroup的使用方法:

  • WaitGroup使用一个整型计数器工作,一般用来记录当前正在工作的线程。
  • Add(n int):该方法用于增加技术器的个数,n表示一次增加的个数。
  • Done():该方法用于减少计数器的个数,一次减少一个。
  • 一般来说,每开启一个goroutine,就使用一次Add(1);每结束一个goroutine,调用一次Done();在需要goroutine合并的地方使用wait()函数进行同步。

几个注意的点:

  • Add()添加的方法总数和最终的Done()调用次数必须匹配,否则出现死锁
  • sync.WaitGroup如果作为函数的参数,必须传递指针。因为sync.WaitGroup是默认传值类型的,这与channel不同!!!

Worker Pool从C++/Java的角度看,可以理解成线程池。但是Golang已经从语言角度支持协程了,一次在这里我们理解成工作任务的集合,是一组等待执行的任务的集合。

构建Worker Pool的流程如下:

  • 创建一组goroutine,用于监听输入的缓冲channel,等待分配任务
  • 向缓冲channel添加任务
  • 等待缓冲channel工作的完成
  • 读取并输出缓冲channel的结果

代码示例:

package main

import (
    "sync"
    "math/rand"
    "time"
    "fmt"
)

type Job struct {
    id       int
    randomNo int
}

type Result struct {
    job      Job
    sumDigit int
}

var jobs = make(chan Job, 10)
var results = make(chan Result, 10)

func digit(number int) int {
    sum := 0
    no := number
    for no != 0 {
        sum += no % 10
        no /= 10
    }
    time.Sleep(2 * time.Second)
    return sum
}

func worker(wg *sync.WaitGroup) {
    for job := range jobs {
        output := Result{job, digit(job.randomNo)}
        results <- output
    }
    wg.Done()
}

func createWorkerPool(noOfWorkers int) {
    var wg sync.WaitGroup
    for i := 0; i < noOfWorkers; i++ {
        wg.Add(1)
        go worker(&wg)
    }
    wg.Wait()
    close(results)
}

func allocate(noOfJobs int) {
    for i := 0; i < noOfJobs; i++ {
        randomNo := rand.Intn(999)
        job := Job{i, randomNo}
        jobs <- job
    }
    close(jobs)   // 一定要记着,所有的jobs都添加完成后,要关闭channel
}

func result(done chan bool) {
    for result := range results {
        fmt.Printf("Job id %d, input random no %d , sum of digits %d\n", result.job.id, result.job.randomNo, result.sumDigit)
    }
    done <- true
}

func main() {
    startTime := time.Now()
    noOfJobs := 100
    go allocate(noOfJobs)
    done := make(chan bool)
    go result(done)
    noOfWorkers := 10
    createWorkerPool(noOfWorkers)
    <-done
    endTime := time.Now()
    diff := endTime.Sub(startTime)
    fmt.Println("total time taken ", diff.Seconds(), "seconds")
}
/*
输出结果不一定严格按照顺序,但是整体上是递增的趋势:
Job id 9, input random no 150 , sum of digits 6
Job id 1, input random no 636 , sum of digits 15
Job id 5, input random no 735 , sum of digits 15
Job id 0, input random no 878 , sum of digits 23
........
Job id 95, input random no 922 , sum of digits 13
Job id 97, input random no 315 , sum of digits 9
Job id 98, input random no 961 , sum of digits 16
Job id 94, input random no 450 , sum of digits 9
total time taken  20.001279005 seconds
*/

代码说明:

  • Jobid表示编号,randomNo表示0-999随机的一个数字
  • Resultjob表示存储的JobsumDigitjob.randomNo的三位数字之和
  • jobs:存储Job类型的channel,作为输入缓冲队列
  • results:存储Result类型的channel,作为结果输出的缓冲队列
  • func digit(number int) int:计算3位数字之和,有延时2秒,模拟长时间工作
  • func worker(wg *sync.WaitGroup):从输入缓冲队列里面取出数据,然后输出到输出缓冲队列里面
  • func createWorkerPool(noOfWorkers int):创建Worker Pool,启动并发执行运算,最终合并所有的工作goroutine
  • func allocate(noOfJobs int):用于创建Job,并输送到jobs队列中
  • func result(done chan bool):从results队列中输出结果

通过流程图来深入了解并发工作模式:

并发模式图

同时并发的几个流程:

  • allocate函数一直在创建Job,如果队列满了就阻塞,直到创建完规定的个数后,关闭jobschannel队列。
  • result函数一直在读取数据并输出,如果results队列空就阻塞,直到createWorkerPool关闭了results队列,并且队列里面没有任何数据。最后还要设置标记的channeltrue,用于通知主程序完毕。
  • createWorkerPool创建出一系列的worker函数,用于处理数据,并且设置合并的位置
  • 所有的worker函数全部从jobs队列里读取数据,然后输送数据到results队列里面,直到jobs里面没有数据而且allocate函数关闭了jobs这个队列

猜你喜欢

转载自blog.csdn.net/qq_35976351/article/details/81908952