Golang之Channel的理解与应用

博客参考自: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确定后就不会更改了。

Wait Group和Worker Pool

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
*/

代码说明:

  • Jobid表示编号,randomNo表示0-999随机的一个数字
  • Resultjob表示存储的JobsumDigitjob.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,如果队列满了就阻塞,直到创建完规定的个数后,关闭jobschannel队列。
  • result函数一直在读取数据并输出,如果results队列空就阻塞,直到createWorkerPool关闭了results队列,并且队列里面没有任何数据。最后还要设置标记的channeltrue,用于通知主程序完毕。
  • createWorkerPool创建出一系列的worker函数,用于处理数据,并且设置合并的位置
  • 所有的worker函数全部从jobs队列里读取数据,然后输送数据到results队列里面,直到jobs里面没有数据而且allocate函数关闭了jobs这个队列
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值