Golang教程:(二十三)缓冲通道和工人池

原文:https://golangbot.com/buffered-channels-worker-pools/

什么是缓冲通道

上一节我们讨论的是基本的无缓冲区的通道,对一个无缓冲的通道进行发送/写入和接收/读取数据是实时阻塞的。事实上我们可以创建一个带缓冲区的通道,往通道中发送数据时,只有当缓冲区满的情况下才会阻塞;类似的,只有我们从通道中读取数据时,只有读到缓冲区空为止才会阻塞。

带缓冲的通道可以通过内置函数make来创建,需要额外指定缓冲区大小。

ch := make(chan type, capacity)  

capacity的值应该大于0,当为0时即我们上节所讨论的无缓冲通道。

让我们举例说明一下:

package main

import (  
    "fmt"
)



func main() {  
    ch := make(chan string, 2)
    ch <- "naveen"
    ch <- "paul"
    fmt.Println(<- ch)
    fmt.Println(<- ch)
}
在playground运行

在上面的例子中我们创建了一个缓冲区大小为2的通道,这样我们就可以往通道中写入2个字符串后才被阻塞,上面的例子中我们分别往通道中写入两个字符串,然后再分别读出。程序打印输出:

naveen  
paul  

让我们再通过另外一个例子能比较好理解缓冲通道的概念。

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)


    }
}

在playground运行

在上面的例子中,首先创建了一个缓冲区大小为2的通道ch := make(chan int, 2),在main协程中创建了一个write的协程。然后main协程sleep了2秒,在这个时间内,write协程并发的运行起来,只要是循环往我们创建的通道中写入0..4。因为我们的缓冲区大小是2,所以当write协程中往通道写入0和1之后,通道就被阻塞,直到最少有一个值被读出。所以我们的程序会先打印出:

successfully wrote 0 to ch  
successfully wrote 1 to ch 

之后当main协程sleep结束后,会循环读取channl中的值,并打印出来,然后再次休眠2秒,然后重复这个过程直到ch被关闭,所以程序会接着打印:

read value 0 from ch  
successfully wrote 2 to ch  

这个过程会持续进行:write协程写入数据->channel缓冲满,阻塞->main协程读取数据->channl缓冲非满,解除阻塞->wirte协程写入shuju->...->直到缓冲区空,关闭channel。最终打印结果如下:

successfully wrote 0 to ch  
successfully wrote 1 to ch  
read value 0 from ch  
successfully wrote 2 to ch  
read value 1 from ch  
successfully wrote 3 to ch  
read value 2 from ch  
successfully wrote 4 to ch  
read value 3 from ch  
read value 4 from ch  

死锁

package main


import (  
    "fmt"
)


func main() {  
    ch := make(chan string, 2)
    ch <- "naveen"
    ch <- "paul"
    ch <- "steve"
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

在playground运行

在上面的例子中,我们往一个缓冲区为2的通道中写入了3个元素,当我们三个元素写入时因为已经达到最大的容量所以协程会阻塞掉,直到有其他协程从channel中读取出来,但是我们这个例子中没有其他协程进行这个工作,所以就会造成死锁的出现,程序会打印下面信息:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:  
main.main()  
    /tmp/sandbox274756028/main.go:11 +0x100

Length vs Capacity

capacity是指缓冲通道能容纳的最大元素的个数,这个值是我们创建缓冲通道时用make函数指定的。length是指当前缓冲通道中的元素个数。上例子演示:

package main


import (  
    "fmt"
)


func main() {  
    ch := make(chan string, 3)
    ch <- "naveen"
    ch <- "paul"
    fmt.Println("capacity is", cap(ch))
    fmt.Println("length is", len(ch))
    fmt.Println("read value", <-ch)
    fmt.Println("new length is", len(ch))
}

在playground运行

在上面的例子中,创建了一个capacity为3的通道,就是说可以容纳3个string元素。当我们写入了2个元素到通道中时,这时候的长度就是2,然后又从通道中读出1个元素,现在通道的长度就变成了1。结果打印如下:

capacity is 3  
length is 2  
read value naveen  
new length is 1

WaitGroup (等待组 ps:翻译不一定准确)

 下面我们要开始讲解并发中的工人池(worker pools)的概念,在此之前我们必须要先了解下waitgroup的概念,因为它被用来实现工人池. 一个等待组(waitgroup)是用来等待一组协程的执行完成。在所有的协程完成之前会一直处于阻塞状态。举个栗子,一个main协程中开启了3个并发的协程,主协程要等待3个子协程结束后再销毁,我们可以用等待组来实现:

package main


import (  
    "fmt"
    "sync"
    "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 go routines finished executing")
}

在playground运行

等待组是一个结构体类型,var wg sync.WaitGroup 创建了一个0值的waitgroup变量。waitgroup是通过计数器来工作的.当我们调用waitgroup的Add方法并传递一个int值给他,waitgroup的计数器就会增加相应的int值。通过调用Done()方法来减少计数器的值。wait()方法会阻塞调用此函数的协程,直到waitgroup中的计数器值变成0.

在上面的例子中我们循环3次调用了wg.Add(1),所以waitgroup的计数器值变为3,main协程调用wg.Wait()方法后就会处于阻塞状态.同时我们也创建了3个子协程,当3个子协程执行结束时会调用wg.Done()3次,计数器值变为0,main协程就会被解除阻塞状态。

需要注意的是我们在go process(i, &wg) 传入的是waitgroup的地址,如果不传入地址的话,那么各个子协程会创建它们各自的一个waitgroup拷贝,而main协程不会得到子协程的结束状态。程序输出如下:

started Goroutine  0  
started Goroutine  1  
Goroutine 0 ended  
Goroutine 2 ended  
Goroutine 1 ended  
All go routines finished executing    

每个人的输出结果可能不尽相同,因为协程具体运行的次序是不同的。

工人池(Worker Pool)的实现

缓冲通道的一个重要用处就是工人池的实现。一个工人池就是一些等待任务的线程集合,一旦他们完成了给他们分配的任务,那就进入可用状态等待下一个任务到来。

我们通过缓冲通道来实现工人池.下面的例子我们会创建一个工人池来实现统计输入数值的数字和。例如输入234.输入就是2+3+4.下面是一些关于我们工人池的核心功能

  • 创建一个协程池用来监听得到job的输入缓冲通道
  • 往输入的缓冲通道中增加任务
  • 当job完成时写入结果到输出的缓冲通道
  • 从输出缓冲通道中读取和打印结果
我们会一步步写出这个例子,以便理解。第一步先创建job和result的结构体
type Job struct {  
    id       int
    randomno int
}
type Result struct {  
    job         Job
    sumofdigits int
}

每一个job结构体拥有一个id和随机值randomno,result拥有一个job元素和统计各位数的和sunofdigits元素值。

下一步我们来创建接收job和输出result的缓冲通道

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

工人协程会监听jobs缓冲通道上到来的任务,一旦任务完成就把结果写入到results的缓冲通道中。

接下来下面digits函数是统计数字的各个位之和。

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

接下来我们创建一个工人协程

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

上面的函数创建了一个“工人”他负责从jobs通道中读取元素,计算结果并网results通道中写入结果。这个方法用一个等待组wg作为参数,当所有jobs被完成时会调用wg的Done()方法。

接下来的crateWorkerPool函数会创建一个工人协程池。

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

上面的函数有一个int输入,指定要创建多少工人。函数中调用wg.Add(1)作为协程增加的计数器,之后把wg作为入参传给worker函数,当完成创建所需要的工人协程之后,就调用wg.Wait()进入阻塞状态。当所有的协程完成执行之后,它会关闭results通道。

现在我们的工人池已经准备好了让我们从头写一个分配工作给工人的函数

func allocate(noOfJobs int) {  
    for i := 0; i < noOfJobs; i++ {
        randomno := rand.Intn(999)
        job := Job{i, randomno}
        jobs <- job
    }
    close(jobs)
}

上面的allocate函数需要一个int作为输入参数指定一共要分发给工人们的工作总数。randomno为一个最大值为998的随机数,通过循环创建Job结构体并写入到jobs通道中,当写入工作全部结束后就关闭jobs通道。

接下来创建读取和输出结果的函数

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.sumofdigits)
    }
    done <- true
}

result函数从result通道中读取出结果值并打印job的id、随机数和统计随机数各位数之和。函数中通过写入done通道值来表示全部结果已经打印完毕。

最后来main函数:

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")
}

函数开始先进行存储了开始执行的时间和最后结束的时间用来计算程序运行的时间差, noOfJobs设置为100,allocate被调用添加jobs到通道。done通道用来获取结果输出完毕的状态。noOfWorkers作为createWorkerPool函数的入参,用来指定创建多少个工人协程。下面给出完整代码:

package main


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


type Job struct {  
    id       int
    randomno int
}
type Result struct {  
    job         Job
    sumofdigits int
}


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


func digits(number int) int {  
    sum := 0
    no := number
    for no != 0 {
        digit := no % 10
        sum += digit
        no /= 10
    }
    time.Sleep(2 * time.Second)
    return sum
}
func worker(wg *sync.WaitGroup) {  
    for job := range jobs {
        output := Result{job, digits(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)
}
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.sumofdigits)
    }
    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")
}

在playground运行

程序打印

Job id 1, input random no 636, sum of digits 15  
Job id 0, input random no 878, sum of digits 23  
Job id 9, input random no 150, sum of digits 6  
...
total time taken  20.01081009 seconds  

一共100行的输出记过会被打印,现在如果把main函数中的noOfWorkers值给为20,会发现total time差不多会降低一半,原因当然是工人池中的工人数量增加了一倍,从jobs协程中读取结果的效率也就多了一倍。你可以自行修改noOfJobs和boOfWorkers的值来查看不同的效果。

这一节就到这里,希望你有愉快的一天!







  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值