Golang-channel-应用
不要通过共享内存来通信,要通过通信来共享内存
在Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Golang从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,所以Golang的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。
1.通道同步
使用通道来同步 Go 协程间的执行状态。这里是一个使用阻塞的接受方式来等待一个 Go 协程的运行结束。
package main
import (
"fmt"
"time"
)
var done chan bool
func do() {
fmt.Println("do something...")
time.Sleep(time.Second)
fmt.Println("done")
// 发送一个信号同步操作完成
done <- true
}
func main() {
done = make(chan bool)
go do()
<-done
fmt.Println("main知晓操作完成")
}
2.通道选择器(select)
Go 的select 可以同时等待多个channel操作:gorouotine和channel以及select的结合。
package main
import (
"fmt"
"time"
)
func main() {
// 建立三个通道
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
// goroutine 1
go func() {
time.Sleep(time.Second * 5)
ch1 <- "one"
}()
// goroutine 2
go func() {
time.Sleep(time.Second * 5)
ch2 <- "two"
}()
// goroutine 3
go func() {
time.Sleep(time.Second * 5)
ch3 <- "three"
}()
// 使用select进行监听
for i := 0; i < 3; i++ {
select {
case msg1 := <-ch1:
fmt.Println("received ch1 message:", msg1)
case msg2 := <-ch2:
fmt.Println("received ch2 message:", msg2)
case msg3 := <-ch3:
fmt.Println("received ch3 message:", msg3)
}
}
}
3.超时处理(time.After)
对于一个连接外部资源,或者其它一些需要花费执行时间的操作的程序而言是很重要的。得益于通道和 select,在 Go中实现超时操作是简洁而优雅的。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func(chs chan string) {
// 模拟操作
time.Sleep(time.Second * 5)
chs <- "do something done."
}(ch)
checkTimeOut(ch, 3)
}
func checkTimeOut(ch chan string, times int) {
select {
case <-time.After(time.Second * time.Duration(times)):
fmt.Printf("%d秒超时了……\r\n", times)
case msg := <-ch:
fmt.Println(msg)
}
}
4.定时器(Timer)
需要在后面一个时刻运行 Go 代码,或者在某段时间间隔内重复运行。Go 的内置 定时器 和 打点器 特性让这些很容易实现。
package main
import (
"fmt"
"time"
)
func main() {
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
// func NewTimer(d Duration) *Timer
timer1 := time.NewTimer(time.Second * 15)
// `<-timer1.C` 直到这个定时器的通道 `C` 的值发送
// 定时器失效的值之前,将一直阻塞。
<-timer1.C
fmt.Println("timer1 start do something")
}
如果需要的仅仅是单纯的等待,使用time.Sleep()即可
定时器可以在定时器失效之前,取消这个定时器。
package main
import (
"fmt"
"time"
)
func main() {
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
// func NewTimer(d Duration) *Timer
timer := time.NewTimer(time.Second * 30)
go func() {
// `<-timer.C` 直到这个定时器的通道 `C` 的值发送
// 定时器失效的值之前,将一直阻塞。
<-timer.C
fmt.Println("timer start do something")
}()
stop := timer.Stop()
if stop {
fmt.Println("timer stopped")
}
}
5.打点器(Ticker)
打点器和定时器的机制有点相似:一个通道用来发送数据,然后在这个通道上使用内置的 range 来迭代值间隔时间发送一次的值。
package main
import (
"fmt"
"time"
)
func main() {
// NewTicker returns a new Ticker containing a channel that will send the
// time with a period specified by the duration argument.
// It adjusts the intervals or drops ticks to make up for slow receivers.
// The duration d must be greater than zero; if not, NewTicker will panic.
// Stop the ticker to release associated resources.
// func NewTicker(d Duration) *Ticker
// 打点器和定时器的机制有点相似:一个通道用来发送数据。
// 这个通道上使用内置的 `range` 来迭代值每隔500ms 发送一次的值。
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick do something:", t)
}
}()
time.Sleep(time.Second * 3)
}
打点器可以被停止,一旦一个打点器停止,将不能再从它的通道中接收到值。
package main
import (
"fmt"
"time"
)
func main() {
// NewTicker returns a new Ticker containing a channel that will send the
// time with a period specified by the duration argument.
// It adjusts the intervals or drops ticks to make up for slow receivers.
// The duration d must be greater than zero; if not, NewTicker will panic.
// Stop the ticker to release associated resources.
// func NewTicker(d Duration) *Ticker
// 打点器和定时器的机制有点相似:一个通道用来发送数据。
// 这个通道上使用内置的 `range` 来迭代值每隔500ms 发送一次的值。
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick do something:", t)
}
}()
time.Sleep(time.Second * 3)
ticker.Stop()
fmt.Println("ticker stoped.")
}
6.工作池
类似于一个线程池。
package main
import (
"fmt"
"runtime"
"time"
)
// 从 `jobs` 通道接收任务,并且通过 `results` 发送对应的结果。我让每个任务间隔 1s 来模仿一个耗时的任务。
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d processing job %d do something\r\n", id, j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
// 理论上同时并行的任务量应该=CPU核心的数量
// worker数量应该为运行主机的实际CPU核心数,
// runtime.NumCPU()获取,
// runtime.GOMAXPROCS()来设置运行的核心数。
runtime.GOMAXPROCS(runtime.NumCPU())
// 为了使用 worker 工作池并且收集运行结果,需要2个通道。
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动了 runtime.NumCPU() 个 worker,还没有传递任务初始是阻塞的。
for w := 1; w <= runtime.NumCPU(); w++ {
go worker(w, jobs, results)
}
// 发送 10 个 `jobs`,然后 `close` 这些通道
// 来表示这些就是所有的任务了。
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
// 最后,收集所有这些任务的返回值。
for a := 1; a <= 10; a++ {
fmt.Println("result:", <-results)
}
}
7.速率限制
速率限制是一个重要的控制服务资源利用和质量的途径。传统的做法是通过redis,设置自增key的expire来实现。Go通过Go协程、通道和打点器优雅地的实现了速率限制。
1.基本速率限制
package main
import (
"fmt"
"time"
)
func main() {
// 假设限制接收请求的处理,将这些请求发送给一个相同的通道。
requests := make(chan int, 5)
for i := 1; i <= 5; i++ {
requests <- i
}
close(requests)
// 这个 `limiter` 通道将每 500ms 接收一个值。这个是速率限制任务中的管理器。
limiter := time.Tick(time.Millisecond * 500)
// 通过在每次请求前阻塞 `limiter` 通道的一个接收,限制每 500ms 执行一次请求。
for req := range requests {
<-limiter
fmt.Printf("request %d : %v handle request to do something.\r\n", req, time.Now())
}
}
2.脉冲型速率限制
有时候某些场景临时进行速率限制,并且不影响整体的速率控制,可以通过通道缓冲来实现。
package main
import (
"fmt"
"time"
)
func main() {
// `burstyLimiter` 通道用来进行 3 次临时的脉冲型速率限制。
burstyLimiter := make(chan time.Time, 3)
// 将通道填充需要临时改变3次的值,做好准备。
for i := 0; i < 3; i++ {
burstyLimiter <- time.Now()
}
// 每 200 ms添加一个新的值到 `burstyLimiter`中,直到达到 3 个的限制。
go func() {
for t := range time.Tick(time.Millisecond * 200) {
burstyLimiter <- t
}
}()
// 模拟超过 5 个的接入请求。刚开始的 3 个将受 `burstyLimiter` 的“脉冲”影响。
burstyRequests := make(chan int, 5)
for i := 1; i <= 5; i++ {
burstyRequests <- i
}
close(burstyRequests)
for req := range burstyRequests {
<-burstyLimiter
fmt.Println("request", req, time.Now())
}
}
8.并发控制
构建一个缓冲型的 channel,容量为 3。接着遍历任务列表,每个任务启动一个 goroutine 去完成。真正执行任务,访问第三方的动作在do() 中完成,在执行 do() 之前,先要从 limit 中拿“许可证”,拿到许可证之后,才能执行 do(),并且在执行完任务,要将“许可证”归还。这样就可以控制同时运行的 goroutine 数。
package main
import (
"fmt"
"time"
)
type handler func()
func (h handler) do() {
fmt.Println("do something...", time.Now())
time.Sleep(time.Second * 1)
}
func main() {
taskList := make([]handler, 10)
limit := make(chan int, 3)
for _, task := range taskList {
// 为什么没有在这里limit <- 1
go func(limit chan int) {
// 如果在外层,就是控制系统 goroutine 的数量,可能会阻塞 for 循环,影响业务逻辑。
limit <- 1
task.do()
// 如果 task.do() 发生 panic,那“许可证”可能就还不回去了,因此需要使用 defer 来保证
defer func(limit <-chan int) {
select {
default:
<-limit
}
}(limit)
}(limit)
}
<-time.After(time.Second * 10)
}