Go教程-工作池|Go主题月

161 阅读6分钟

本文为翻译文章

原文地址:golangbot.com/buffered-ch…

工作池

缓冲通道的重要用途之一是工作池的实现。通常,工作池是等待任务分配给它们的线程的集合。一旦完成分配的任务,他们就可以再次用于下一个任务。我们将使用缓冲通道实现工作池。我们的工作池将执行查找输入数字的位数之和的任务。例如,如果传递了 234,则输出将为 9(2 + 3 + 4)。工作池的输入将是伪随机整数列表。

以下是我们工作池的核心功能:

  • 创建 Goroutine 池,该 Goroutine 池在输入缓冲通道上侦听以等待分配作业
  • 将作业添加到输入缓冲通道
  • 作业完成后将结果写入输出缓冲通道
  • 从输出缓冲通道读取和打印结果

我们将逐步编写此程序,以使其更易于理解。

第一步将是创建代表工作和结果的结构。

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

每个 Job 结构都有一个 ID 和一个 randomno,必须为其计算各个数字的总和。

Result 结构体具有一个 job 字段,该字段是在 sumofdigits 字段中保存结果(单个数字的总和)的作业。

下一步是创建用于接收作业和写入输出的缓冲通道。

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

Worker Goroutines 在作业缓冲通道上侦听新任务。任务完成后,结果将写入结果缓冲通道。

下面的 digits 函数会执行实际工作,查找整数的各个数字之和,然后将其返回。我们将为此功能增加 2 秒钟的睡眠时间,以模拟该功能需要花费一些时间来计算结果的事实。

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
}

接下来,我们将编写一个创建工作程序 Goroutine 的函数。

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

上面的函数创建一个工作程序,该工作程序从作业通道中读取,使用当前作业和 digits 函数的返回值创建一个 Result 结构,然后将结果写入 results 缓冲通道。此函数将 WaitGroup wg 作为参数,当所有作业完成时,它将在其上调用 Done() 方法。

createWorkerPool 函数将创建工作程序 Goroutine 池。

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

上面的函数将要创建工作池的大小作为参数。在创建 Goroutine 来递增 WaitGroup 计数器之前,它将调用 wg.Add(1)。然后,它通过将 WaitGroup wg 的指针传递给 worker 函数来创建 worker Goroutines。创建所需的工作程序 Goroutines 之后,它将通过调用 wg.Wait() 等待所有 Goroutines 完成执行。在所有 Goroutine 完成执行后,由于所有 Goroutine 完成了执行,因此它关闭了结果通道,并且没有其他人将进一步写入结果通道。

现在我们已经准备好工作池,让我们继续编写可以将作业分配给工作人员的函数。

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

上面的分配函数将要创建的作业数作为输入参数,生成最大值为 998 的伪随机数,使用随机数和 for 循环计数器 i 作为 id创建 Job 结构,然后将其写入作业 渠道。写入所有作业后,它将关闭作业通道。

下一步将是创建读取结果通道并打印输出的函数。

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 函数读取 results 通道并打印作业 ID,输入随机数和随机数的位数之和。result 函数还将 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")
}

我们首先在 mian 函数中存储程序的执行开始时间,然后在最后一行中计算 endTime 和 startTime 之间的时间差,并显示该程序花费的总时间。 这是必需的,因为我们将通过更改 Goroutine 的数量来进行一些基准测试。

将 noOfJobs 设置为 100,然后调用 allocate 将作业添加到作业通道。

然后创建完工通道并将其传递给结果 Goroutine,以便它可以开始打印输出并在所有内容都打印完后通知。

最终,通过调用 createWorkerPool 函数创建了一个包含 10 个辅助 Goroutine 的池,然后 main 在 done 通道上等待所有结果被打印出来。

这是完整程序供您参考。我也导入了必要的软件包。

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

请在本地计算机上运行此程序,以更准确地计算总时间。

程序输出:

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 个作业相对应地打印总共 100 行,然后最后在最后一行中打印程序运行所花费的总时间。您的输出将与我的输出不同,因为 Goroutines 可以以任何顺序运行,并且总时间也将根据硬件而有所不同。就我而言,该程序大约需要 20 秒钟才能完成。

现在让我们将 main 函数中的 noOfWorkers 增加到 20。我们使工作池里的数增加了一倍。由于 work Goroutines 增加了(准确地说是增加了一倍),因此完成程序所需的总时间应该减少(准确地说减少了一半)。在我的情况下,它变成了 10.004364685 秒,程序被打印出来了,

...
total time taken  10.004364685 seconds  

现在我们可以理解,随着工人 Goroutine 数量的增加,完成工作所需的总时间减少了。我将其作为练习,让您在主要函数中使用 noOfJobs 和 noOfWorkers 来获得不同的值并分析结果。