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)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值