博客参考自: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
确定后就不会更改了。
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
*/
代码说明:
Job
:id
表示编号,randomNo
表示0-999随机的一个数字Result
:job
表示存储的Job
,sumDigit
是job.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
,如果队列满了就阻塞,直到创建完规定的个数后,关闭jobs
的channel
队列。result
函数一直在读取数据并输出,如果results
队列空就阻塞,直到createWorkerPool
关闭了results
队列,并且队列里面没有任何数据。最后还要设置标记的channel
为true
,用于通知主程序完毕。createWorkerPool
创建出一系列的worker
函数,用于处理数据,并且设置合并的位置worker
函数全部从jobs
队列里读取数据,然后输送数据到results
队列里面,直到jobs
里面没有数据而且allocate
函数关闭了jobs
这个队列