创建一个有缓冲(Buffer)的信道。只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。
通过向 make
函数再传递一个表示容量的参数(指定缓冲的大小),可以创建缓冲信道。
ch := make(chan type, capacity)
上面语法中的 capacity
应该大于 0。无缓冲信道的容量默认为 0
import (
"fmt"
)
func main() {
// 写入两次,就不能再写入了,否则报错。
ch := make(chan string, 2)
ch <- "naveen"
ch <- "paul"
fmt.Println(<- ch)
ch <- "paul2"
fmt.Println(<- ch)
fmt.Println(<- ch)
}
结果:
naveen
paul
paul2
一个协程写入数据,一个协程读取数据
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 string, 2)
ch <- "naveen"
ch <- "paul"
//ch <- "paul2" // 打开会造成死锁,信道写满,没有其他协程读取,再次写入发生死锁
fmt.Println(<- ch)
ch <- "paul2"
fmt.Println(<- ch)
fmt.Println(<- ch)
ch2 := make(chan int,2)
go write(ch2)
// 尝试把这句话注释掉,看下结果,进行分析
time.Sleep(2 * time.Second)
for v := range ch2 {
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
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
write协程先写入0 1,然后阻塞,主协程进行读取,然后主协程休眠2秒,开始读取数据
读取一次,休眠两秒,知道结束
所以先打印
successfully wrote 0 to ch
successfully wrote 1 to ch
然后
read value 0 from ch
successfully wrote 2 to ch
依次类推,直到结束
长度 vs 容量
缓冲信道的容量
是指信道可以存储的值的数量
。我们在使用 make 函数创建缓冲信道的时候会指定容量大小
。
缓冲信道的长度
是指信道中当前排队的元素个数
。
ch := make(chan string, 3)
ch <- "naveen"
ch <- "paul"
fmt.Println("capacity is", cap(ch)) // 容量3
fmt.Println("length is", len(ch)) // 长度2
fmt.Println("read value", <-ch) // 读取一次,容量3
fmt.Println("new length is", len(ch)) // 长度1
WaitGroup
工作池(Worker Pools),而 WaitGroup 用于实现工作池。
WaitGroup 用于等待一批 Go 协程执行结束。程序控制会一直阻塞,直到这些协程全部执行完毕
。假设我们有 3 个并发执行的 Go 协程(由 Go 主协程生成)。Go 主协程需要等待这 3 个协程执行结束后,才会终止。这就可以用 WaitGroup 来实现。
package main
import (
"fmt"
"time"
"sync"
)
func process(index int,wait *sync.WaitGroup) {
fmt.Printf("goroutine start %d \n",index)
time.Sleep(1*time.Second)
fmt.Printf("Goroutine %d ended\n", index)
wait.Done()
}
func main() {
var wait sync.WaitGroup
// 等待三个协程运行完,再走主线程
for i := 0; i < 3; i++ {
wait.Add(1)
//time.Sleep(1*time.Second)
go process(i,&wait)
}
fmt.Println("=================")
wait.Wait()
fmt.Printf("main goroutine")
}
结果:
goroutine start 0
goroutine start 1
=================
goroutine start 2
Goroutine 1 ended
Goroutine 2 ended
Goroutine 0 ended
main goroutine
当我们调用 WaitGroup
的 Add
并传递一个 int
时,WaitGroup
的计数器会加上 Add
的传参。要减少计数器,可以调用 WaitGroup
的 Done()
方法。Wait()
方法会阻塞调用它的 Go 协程,直到计数器变为 0 后才会停止阻塞
。
传递 wg
的地址是很重要的。如果没有传递 wg 的地址
,那么每个 Go 协程将会得到一个 WaitGroup
值的拷贝,因而当它们执行结束时,main
函数并不会知道。
工作池的实现
缓冲信道的重要应用之一就是实现工作池。
一般而言,工作池就是一组等待任务分配的线程。一旦完成了所分配的任务,这些线程可继续等待任务的分配
我们会使用缓冲信道来实现工作池。我们工作池的任务是计算所输入数字的每一位的和。例如,如果输入 234,结果会是 9
我们工作池的核心功能如下:
创建一个 Go 协程池,监听一个等待作业分配的输入型缓冲信道。
将作业添加到该输入型缓冲信道中。
作业完成后,再将结果写入一个输出型缓冲信道。
从输出型缓冲信道读取并打印结果。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func process(index int,wait *sync.WaitGroup) {
fmt.Printf("goroutine start %d \n",index)
time.Sleep(1*time.Second)
fmt.Printf("Goroutine %d ended\n", index)
wait.Done()
}
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() {
var wait sync.WaitGroup
// 等待三个协程运行完,再走主线程
for i := 0; i < 3; i++ {
wait.Add(1)
//time.Sleep(1*time.Second)
go process(i,&wait)
}
fmt.Println("=================")
wait.Wait()
fmt.Printf("main goroutine")
startTime := time.Now()
// 数据写入工作信道
noOfJobs := 100
go allocate(noOfJobs)
// 结果协程,结果信道中读取数据
done := make(chan bool)
go result(done)
// 工作池,里面创建工作协程,循环读取工作信道数据,写入到结果信道,完成后关闭工作池,关闭结果信道
noOfWorkers := 100
createWorkerPool(noOfWorkers)
// 读取结果信道
<-done
endTime := time.Now()
diff := endTime.Sub(startTime)
fmt.Println("total time taken ", diff.Seconds(), "seconds")
}
结果:
Job id 30, input random no 562 , sum of digits 13
Job id 86, input random no 236 , sum of digits 11
Job id 85, input random no 421 , sum of digits 7
。。。。
total time taken 20.0043116 seconds
在 main 函数里修改 noOfJobs
和 noOfWorkers
的值,并试着去分析一下结果
noOfWorkers := 100