G7.1 Go语言中使用通道chan实现goroutine池

一般的计算机语言在处理并发任务时,经常会按需创建线程,例如在Web服务器应用中,接收到一个网络请求(network request)就会新建一个线程来处理。但系统资源有限,一般会控制所建线程个数的上限,而控制上限的一种方法就是使用“线程池”:例如我们把可用的n个线程抽象成一个线程池,该池中就有n个线程,需要处理请求时会从其中取出一个线程来使用,使用完毕后该线程将被放回池中,如果池中的线程都已经被取用,则新来的请求将被阻塞等待直至有可用的线程为止,另一种处理的方法是直接向该请求者报告异常,请求将被中止。在Go语言中线程对应的是goroutine,本节将演示如何用通道来实现goroutine池,由于没有必要直接将goroutine对应的函数直接放入通道中,因此也可用类似令牌的方式来控制goroutine的最大数量,形成虚拟的goroutine池,也可以看作将goroutine池转换成了令牌池。


package main


import (

  "math/rand"

  "time"

  t "tools"

)


// Request1 是自定义的结构类型

// 用于发送请求时传递所需的所有数据

type Request1 struct {

  ID      int           //本次请求的编码,是随机产生的整数

  Count   int           //计算多少次

  Response chan []string //用于存放请求结果的通道

}


// 令牌池

var poolG chan int


// 缓存请求的通道

var requestChannelG chan Request1


// 发送请求并等待请求执行结果的函数

func sendRequest(countA int) {

  idT := rand.Intn(10000)


  responseChanT := make(chan []string, 1)


  defer close(responseChanT)


  requestChannelG <- Request1{ID: idT, Count:countA, Response: responseChanT}


  responseT := <-responseChanT


  t.Printfln("goroutine(ID: %v, Count: %v)结果: %#v", idT, countA, responseT)


}


// 处理具体每个请求的函数

func doRequest(requestA Request1) {

  resultT := 0

  for i := 0; i < requestA.Count; i++ {

       resultT = i + requestA.Count

  }


  requestA.Response <- []string{"请求成功", t.IntToString(resultT)}


  poolG <- 0

}


// 处理请求队列(通道)的函数

// 根据令牌池是否有剩余令牌来决定是否启动处理具体请求的goroutine

func processRequests() {

  for {

       requestT := <-requestChannelG


       if len(poolG) > 0 {

             <-poolG

             go doRequest(requestT)

       } else {

             requestT.Response <-[]string{"请求失败", "goroutine池已满"}

       }


  }

}


func main() {


  //为令牌池分配容量,容量虽然为10

  //但可用令牌是根据后面放入的令牌个数来确定的

  poolG = make(chan int, 10)


  //为请求缓冲通道分配100个的容量

  //如果缓冲通道内超过100个请求,下一个请求将被阻塞

  requestChannelG = make(chan Request1, 100)


  //确保通道退出时会被关闭

  defer close(requestChannelG)

  defer close(poolG)


  //启动请求处理主goroutine

  go processRequests()


  //在令牌池中放入5块令牌

  for i := 0; i < 5; i++ {

       poolG <- 0

  }


  //模拟发送20个请求

  for i := 0; i < 20; i++ {

       go sendRequest(1000)

  }


  //主线程死循环以保证处理请求的goroutine一直执行

  //需要用Ctrl+C键来退出程序运行

  for {

       time.Sleep(100 * time.Millisecond)

  }


}

代码 11‑9 goroutine2/goroutine2.go


代码11‑9中,使用通道poolG作为令牌池,来控制处理请求队列的主任务processRequests能够生成子任务(也就是由处理具体每个请求的doRequest函数生成的goroutine)的数量。代码中的要点说明如下:


-> poolG通道代表令牌池,里面有几块令牌就表明可以启动多少个处理请求的goroutine,也即是令牌池的可用令牌数限制了同时可以并发处理的请求数;注意poolG的容量并不代表可用令牌数,而是该通道中的数值项数表示令牌数;

-> requestChannelG通道用于缓存收到的所有请求,该通道的数值项类型是我们自定义的Request1,发送的请求必须符合该类型,Request1类型中的Response字段又是一个通道类型,用于接收该请求的响应信息;

-> sendRequest函数用于模拟创建单个的请求,它将产生一个随机的请求标识号填入Request1.ID字段中,然后指定要求计算的次数放入Request1.Count,还会创建用于接收响应信息的通道放入Request1.Response中,之后该函数就会通过该通道等待接收返回的响应信息后输出,最后退出该goroutine的运行;

-> processRequests函数用于处理请求队列和分派goroutine来处理,它会循环读取requestChannelG通道中的请求,一旦接收到一个请求时,它会首先判断是否还有可用的令牌,这是通过len(poolG)表达式来获取poolG通道的当前长度来判断的,如果不为0,则表明通道里面有数值项,也就代表还有可用令牌,此时通过<-poolG语句获取一块令牌,然后启动具体处理单个请求的doRequest函数的goroutine来进行处理;如果没有可用令牌,则直接向该请求的响应通道中写入相应信息表示请求失败;

-> doRequest函数用于处理单个请求,它在完成了指定的计算任务之后,会在该请求的响应通道中写入请求成功的信息和计算结果,然后在退出之前交还令牌(通过向poolG通道中写入一个数据来实现);

-> 主函数main中,首先定义了令牌池poolG通道和请求队列requestChannelG通道,并为它们分配了容量;然后启动processRequests函数的goroutine来循环处理请求;之后往令牌池poolG中放入了5块令牌,代表着本程序将能够同时并发处理5个请求;然后模拟发送了20个请求来测试其请求处理能力;最后用一个无限循环(也叫死循环)来保证程序一直运行,并通过time.Sleep函数来定时休眠一下,这样可以保证程序不占用太多的系统资源,并且在休眠时其他goroutine能得到更多的调度执行时间。注意,由于main函数最后的死循环,本程序必须用Ctrl-C快捷键才能终止运行。


该代码执行结果如下:


goroutine(ID: 3300, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 8081, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 1211, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 456, Count: 1000)结果: []string{"请求失败", "goroutine池已空"}

goroutine(ID: 2540, Count: 1000)结果: []string{"请求失败", "goroutine池已空"}

goroutine(ID: 7887, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 1847, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 2081, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 4425, Count: 1000)结果: []string{"请求失败", "goroutine池已空"}

goroutine(ID: 1318, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 694, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 8511, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 5089, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 8162, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 4728, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 3274, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 9106, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 1445, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 3237, Count: 1000)结果: []string{"请求成功", "1999"}

goroutine(ID: 4059, Count: 1000)结果: []string{"请求成功", "1999"}


可以看出,20个模拟发送的请求大部分被处理了,但有几个请求由于令牌池当时为空,所以没有被处理。


本例主要说明的要点包括:


-> 利用通道可以实现令牌池或虚拟的goroutine池等,用于控制goroutine等资源的数量上限,避免系统资源被无限制地使用;

-> 通道的数值项类型也可以是复合类型或者自定义的结构类型,这使得通道的可用性大大增强,例如,可以使用结构类型的通道在做goroutine归并时分别判断出不同类型的goroutine执行完毕的个数等。

你可能感兴趣的:(G7.1 Go语言中使用通道chan实现goroutine池)