select基础知识
select
是 Go 语言中用于处理通道操作的控制结构,它类似于 switch
语句,但专门用于通道的选择。select
语句使得一个 goroutine 可以等待多个通道操作,当其中任意一个通道操作可以进行时,就会执行相应的 case
分支。
select语句语法如下:
select {
case channel1 <- value1:
// 如果 channel1 可以写入,执行这里
case value2 := <-channel2:
// 如果 channel2 可以读取,执行这里
case value3, ok := <-channel3:
// 如果 channel3 被关闭,并且有数据可读,执行这里
case <-time.After(time.Second):
// 在超时时间内没有任何 case 可执行,执行这里
default:
// 如果没有任何 case 可执行,执行这里
}
特性:
- 如果多个
case
同时满足条件,Go 会随机选择一个执行。 - 如果没有
case
可以执行,且存在default
分支,则执行default
。 - 如果没有
default
分支,select
会阻塞,直到至少有一个case
可执行。 select
可以和for
循环一起使用,用于不断地处理通道操作。
select
的主要用途是处理并发编程中的多个通道操作,例如处理超时、非阻塞通信等场景。
1. 超时控制
超时会比程序请求失败还可怕,为了避免主线程阻塞可以在select中设置超时中断。
示例代码如下:通过多路选择等待任务完成,如果超时就直接执行其他的处理程序
package main
import (
"fmt"
"time"
)
// 超时控制
func main() {
select {
case re := <-AsynService():
fmt.Print("任务完成", re)
case <-time.After(time.Millisecond * 3000):
fmt.Print("超时啦")
//default:
// fmt.Print("不能阻塞")
}
}
// 服务
func service() string {
time.Sleep(time.Millisecond * 3000)
return "finish"
}
// 异步启动服务
func AsynService() chan string {
rechan := make(chan string, 1)
go func() {
res := service()
time.Sleep(time.Millisecond * 1000)
rechan <- res
}()
return rechan
}
2. 任务取消
(1)获取取消通知
// 3.select判断任务是否取消
func isCanceled(cn chan struct{}) bool {
select {
case <-cn:
fmt.Println("任务取消")
return true
default:
fmt.Println("任务不取消,继续执行")
return false
}
}
(2)发送取消消息
// 1.普通向cancel通道发送取消通知,这种做法需要事先知道有多少个正在执行的任务
func cancel1(cn chan struct{}) {
cn <- struct{}{}
}
// 2.向采取close方法关闭所有任务
func cancel2(cn chan struct{}) {
close(cn)
}
(3)测试
// 测试任务取消
func TestCancel(t *testing.T) {
cn := make(chan struct{})
for i := 1; i < 6; i++ {
go func(cn chan struct{}) {
for {
if isCanceled(cn) {
break
} else {
time.Sleep(time.Millisecond * 1000)
}
}
fmt.Println("任务取消")
}(cn)
}
cancel2(cn)
}
六个go程进行监听任务是否取消,普通发送取消通知只会取消一个go程,关闭通道可以取消所有在监听的go程。
3. Context任务取消
(1)Context介绍
在 Go 语言中,context.Context
是一个标准库中非常常用的接口,它提供了在多个 goroutine 之间传递请求范围的截止日期、取消信号、存储值等信息的途径。context.Context
主要用于在函数之间传递请求的截止日期、取消信号、跟踪信息以及其他请求范围的值。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline()
:返回Context
的截止日期(即取消的时间点)和一个布尔值,表示是否设置了截止日期。Done()
:返回一个<-chan struct{}
类型的通道,该通道关闭时表示Context
被取消或者达到了截止日期。Err()
:返回一个错误,表示Context
被取消的原因。Value(key interface{})
:根据给定的键返回相关联的值,通常用于传递请求范围的值。
context
包还提供了一些函数用于创建和操作 Context
:
context.Background()
:返回一个空的Context
,常用于表示整个请求生命周期。context.TODO()
:TODO
表示 "to do",返回一个空的、不可取消的Context
。context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
:返回一个可取消的Context
和一个对应的CancelFunc
,可以用来取消该Context
。context.WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
:返回一个带有截止日期的Context
。context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
:返回一个带有超时时间的Context
。context.WithValue(parent Context, key, val interface{}) Context
:返回一个包含指定键值对的Context
。
(2)任务取消
发送取消消息:
func isCanceled2(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
我们可以看一下ctx.Done()的源码:
Done()方法返回一个channel,直接读取这个channel将会被阻塞,让我们看一下cancelCtx源码:
由上面的结构体可以知道这里c.done是一个原子操作的值,采用懒加载方法,被第一次调用的cancel方法关闭。
调用Done函数时已经存在一个取消通道时就直接返回,当是第一个调用的就创建channel并返回,都是并发安全的。
测试:
func TestContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i < 6; i++ {
go func(i int, ctx context.Context) {
for {
if isCanceled2(ctx) {
break
} else {
time.Sleep(time.Millisecond * 100)
}
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
cancel()
time.Sleep(time.Second * 1)
}
这里通过context.Background()函数获得顶级context,通过WithCancel函数获取一个子上下文和一个取消函数,通过取消顶级context可以取消所有子上下文达到任务取消目的,或者是子上下文其自身取消。
让我们看一下cancel方法:cancel方法关闭c.done也就是关闭了这个chan通道通知任务取消,同时也递归取消所有的子上下文,如果removeFromParent参数为true将会从父context移除掉当前子context。
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}