文章目录
协程
早在接触 go 之前,就听过大名鼎鼎的协程,现在终于有机会一探究竟了。支持协程的语言并不只有 go 一种,比如 Python,Kotlin 也都支持协程。Java 目前不支持协程,只能通过第三方库实现协程,但是未来可能会支持。
需要注意的是,虽然协程(Coroutine)在这些年突然很火,但协程并不是一个新的概念,相比线程也并不能说协程是更高级的技术,只是各有千秋。协程是在1963年由Melvin E. Conway USAF, Bedford, MA等人提出的一个概念,协程的概念是早于线程(Thread)提出的。由于协程是 非抢占式 的调度,无法实现公平的任务调用,也无法直接利用多核优势,但在资源消耗上,协程则是极低的,一个线程的内存在 MB 级别,而协程只需要 KB 级别。
线程和协程的对比
创建时默认的 stack 的大小
- JDK5 以后 Java Thread stack默认为1M
- Groutine 的 Stack 初始化大小为2K
和 KSE (Kernel Space Entity,操作系统的线程) 的对应关系
- Java Thread 是 1:1
- Groutine 是 M:N
由于 Java 的线程与系统线程是1:1的关系,用户空间的线程调度 = 系统的线程调度,用户空间进行线程调度时涉及到不同线程之间的上下文切换,开销较大。go 中的协程与系统线程的对应关系是 M:N,一个系统线程可能对应go 语言中的N个协程,N个协程之间的切换只是在一个线程上进行切换,上下文切换较小。
示例
在没有使用并发之前
package goroutine
import (
"fmt"
"testing"
)
func TestGoroutine(t *testing.T) {
for i := 0; i < 10; i++ {
printNum(i)
}
}
func printNum(i int) {
fmt.Println(i)
}
输出
=== RUN TestGoroutine
0
1
2
3
4
5
6
7
8
9
--- PASS: TestGoroutine (0.00s)
PASS
使用并发之后(go 中只需要在函数前面加上 go就开启协程了)。在这里不得不说 go 的语法简洁,只需要在一个函数前加上 go,就代表这个函数会被别的协程异步执行。
package goroutine
import (
"fmt"
"testing"
)
func TestGoroutine(t *testing.T) {
for i := 0; i < 10; i++ {
go printNum(i)
}
}
func printNum(i int) {
fmt.Println(i)
}
输出如下,由于是并发执行,所以输出的顺序并不固定。
=== RUN TestGoroutine
0
2
5
4
6
9
3
--- PASS: TestGoroutine (0.00s)
1
7
PASS
8
共享内存并发机制
使用互斥锁 Mutex
在其它的语言中,多个线程并发访问共享内存,通常都是通过加锁的形式来进行控制的。在go中同样可以通过锁来控制对共享变量的访问。
第一段代码,没有使用互斥锁来访问共享变量。
func TestCounter(t *testing.T) {
counter := 0
for i := 0; i < 5000; i++ {
go func() {
// 共享变量
counter++
}()
}
// 等待协程执行完毕
time.Sleep(time.Second * 1)
t.Logf("counter = %d", counter)
}
输出,加了5000次,按道理共享变量 counter 的值应该是5000,但是事实并非如此,这主要是因为多个线程并发访问同一块内存时,没有加入任何的同步机制。
=== RUN TestCounter
share_memory_test.go:19: counter = 4803
--- PASS: TestCounter (1.00s)
PASS
使用 Mutex 互斥锁
// mutex 互斥锁
func TestCounterWithMutex(t *testing.T) {
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
go func() {
// 代码1等效于代码2,defer函数类似于Java中的finally
defer func() {
mut.Unlock()
}()
mut.Lock()
// 共享变量
counter++
// 代码2
//mut.Unlock()
}()
}
// 等待协程执行完毕
time.Sleep(time.Second * 1)
t.Logf("counter = %d", counter)
}
输出
=== RUN TestCounterWithMutex
share_memory_test.go:52: counter = 5000
--- PASS: TestCounterWithMutex (1.00s)
PASS
使用 WaitGroup
WaitGroup 有Java中 join 或者 CountDownLatch 的作用,也有点类似于信号量。上述的示例中,主协程中等待其它协程执行完毕,都是靠延时,首先需预估其它协程在这段时间内能执行完毕所需要的时间,难免不精确,这里就可以采用 WaitGroup。
// WaitGroup 有Java中 join 的作用,也有类似于信号量的作用
func TestCounterWithWaitGroup(t *testing.T) {
var wg sync.WaitGroup
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
// WaitGroup的计数值加1
wg.Add(1)
go func() {
// 代码1等效于代码2
defer func() {
mut.Unlock()
}()
mut.Lock()
// 共享变量
counter++
// WaitGroup的计数值减一
wg.Done()
}()
}
// 等待所有任务都执行完毕,才会继续向下执行,类似于Java中的join或CountDownLatch
// 当 WaitGroup 的计数值为0,才会继续往下执行
wg.Wait()
t.Logf("counter = %d", counter)
}
输出
=== RUN TestCounterWithWaitGroup
share_memory_test.go:78: counter = 5000
--- PASS: TestCounterWithWaitGroup (0.00s)
PASS
CSP 并发机制
CSP 并发机制 是Go语言独有的。不去考虑 CS P晦涩的理论以及英文单词,简单来说,就是一种利用 Channel 来实现协程之间通信的方式,类似于管道通信,然后结合 goroutine,就形成了一种go语言独特的并发机制。
Channel的两种通信方式
方法一:通信双方都必须在 Channel 两端等待,任何一端不在都会造成阻塞。
方法二:Channel 中使用 Buffer, Buffer 有一定的容量限制。不需要通信双方都在 Channel 两端等待,发送方发送数据时不需要接受方等着接收,先发送到 Buffer 中,接收方去 Buffer 中取即可。
使用 channel 异步返回数据
先看第一段代码
func service() string {
time.Sleep(time.Millisecond * 1000)
return "Service Done"
}
func otherTask() {
fmt.Println("other task is working")
time.Sleep(time.Millisecond * 1000)
fmt.Println("other task is done")
}
func TestService(t *testing.T) {
otherTask()
fmt.Println(service())
}
输出如下。从输出中可以发现执行代码用时2S,这也是在预料中的,service() 函数中延迟了 1S,otherTask() 函数中延迟了 1S ,这两个函数是同步的方式进行执行的,所以总共 耗时2S。
=== RUN TestService
other task is working
other task is done
Service Done
--- PASS: TestService (2.00s)
PASS
第二段代码
func service() string {
time.Sleep(time.Millisecond * 1000)
return "Service Done"
}
func otherTask() {
fmt.Println("other task is working")
time.Sleep(time.Millisecond * 1000)
fmt.Println("other task is done")
}
func AsyncService() chan string {
// 使用 make 创建 channel
retCh := make(chan string)
go func() {
ret := service()
fmt.Println("return result")
// 将返回结果 ret 写入channel中
retCh <- ret
fmt.Println("AsyncService is done")
}()
return retCh
}
func TestAsyncService(t *testing.T) {
// 先得到一个 channel
retch := AsyncService()
// 在 retch 这个channel 的数据还没有准备好的时候先执行其它任务
otherTask()
// 取数据时会如果 channel 中没有数据,则会阻塞。从channel中<-retch
fmt.Println(<-retch)
}
输出。从输出中可以发现整个执行的过程只花了1S。首先执行 retch := AsyncService() 得到一个 Channel,Channel 中此时还没有数据,因为在执行Service 方法获取数据时需要演示1S(模拟耗时操作),因此不可能马上得到数据。在这段时间内,先执行 otherTask,otherTask 耗时 1S,在这个时间段内 Service 就能准备好数据放入 Channel 中,然后使用执行 <-retch 就能从 Channel 中获取数据。
=== RUN TestAsyncService
other task is working
return result
other task is done
Service Done
AsyncService is done
--- PASS: TestAsyncService (1.00s)
PASS
结合上面的输出结果,可以发现 return result 在 otherTask 结束之前就已经执行了,接下来的步骤就是阻塞在 retCh <- ret 这里,Channel 中需要阻塞等待发送方准备好数据,然后等待接收方获取数据,才能继续往下执行。上面的输出结果是最后打印 AsyncService is done 的,表明了在
TestAsyncService 的 fmt.Println(<-retch) 中获取到 Channel 中的数据之前,AsyncService 就一直阻塞在retCh <- ret 这句代码。
在上面讲述 Channel 的两种通信方式时,讲到了带有 Buffer 的 Channel 就可以尽量避免阻塞。创建带 Buffer 的 Channel 只需要在使用 make 函数创建时指定 Buffer 的大小,创建的 channel 就是带有 Buffer 的。
func AsyncService() chan string {
// 创建 buffer 大小为1的channel
retCh := make(chan string, 1)
go func() {
ret := service()
fmt.Println("return result")
// 将返回结果 ret 写入channel中
retCh <- ret
fmt.Println("AsyncService is done")
}()
return retCh
}
其输出如下图的右半部分所示。可以发现在通过 fmt.Println(<-retch) 获取数据打印 Service Done 之前, AsyncService is done 就已经被打印了,说明在调用过程中,AsyncService 这个函数并未阻塞在 retCh <- ret 这句代码
多路选择和超时控制
go 语言中提供了 select 机制进行多路控制以及超时控制。
话不多说,看代码。
如下,定义了两个任务 AsyncService1 耗时约 1000 ms,AsyncService2 耗时约 100 ms。
func AsyncService1() chan string {
// 创建 buffer 大小为1的channel
retCh := make(chan string, 1)
go func() {
// 延时 1000ms,模拟耗时操作
time.Sleep(time.Millisecond * 1000)
ret := "AsyncService1 Done"
// 将 ret 写入channel中
retCh <- ret
fmt.Println("AsyncService1 is done")
}()
return retCh
}
// 100 ms
func AsyncService2() chan string {
// 创建 buffer 大小为1的channel
retCh := make(chan string, 1)
go func() {
// 延时 100ms,模拟耗时操作
time.Sleep(time.Millisecond * 100)
ret := "AsyncService2 Done"
// 将 ret 写入channel中
retCh <- ret
fmt.Println("AsyncService2 is done")
}()
return retCh
}
使用 select 进行多路控制并设定相应的超时时间,如下设置的超时时间为50ms,而 AsyncService1 耗时 约1000 ms,AsyncService2 耗时约 100 ms,显然在超时时间限制内两个函数都无法返回数据。
func TestSelect(t *testing.T) {
select {
case ret := <-AsyncService1():
t.Log(ret)
case ret := <-AsyncService2():
t.Log(ret)
// 进行超时控制,在给定的时间内都没有返回,则运行这个case分支
case <-time.After(time.Millisecond * 50):
t.Error("Time out,no return data was obtained")
}
}
输出如下,运行了超时分支。
=== RUN TestSelect
select_test.go:46: Time out,no return data was obtained
--- FAIL: TestSelect (0.05s)
FAIL
如下,调整了超时的时间限制,设置为 200 ms,在这个时间段内,AsyncService2 能完成任务并返回数据。
func TestSelect(t *testing.T) {
select {
case ret := <-AsyncService1():
t.Log(ret)
case ret := <-AsyncService2():
t.Log(ret)
// 进行超时控制,在给定的时间内都没有返回,则运行这个case分支
case <-time.After(time.Millisecond * 200):
t.Error("Time out,no return data was obtained")
}
}
输出如下,AsyncService2 运行完成了,符合预期。
=== RUN TestSelect
AsyncService2 is done
select_test.go:43: AsyncService2 Done
--- PASS: TestSelect (0.10s)
PASS
继续增大超时时间,超时时间设置为 1200 ms,这段时间足够 AsyncService1 执行并返回数据,但是由于 AsyncService2 已经返回数据了,所以 AsyncService1 的分支还是不会被执行。
func TestSelect(t *testing.T) {
select {
case ret := <-AsyncService1():
t.Log(ret)
case ret := <-AsyncService2():
t.Log(ret)
// 进行超时控制,在给定的时间内都没有返回,则运行这个case分支
case <-time.After(time.Millisecond * 1200):
t.Error("Time out,no return data was obtained")
}
}
输出如下,与超时时间为 200ms 的输出保持一致。
=== RUN TestSelect
AsyncService2 is done
select_test.go:43: AsyncService2 Done
--- PASS: TestSelect (0.10s)
PASS
再来看看 default,不管是执行 AsyncService1 还是执行 AsyncService2 都需要耗费一定的时间,都不能立即返回数据,因此会选择执行 default 分支。
func TestSelect(t *testing.T) {
select {
case ret := <-AsyncService1():
t.Log(ret)
case ret := <-AsyncService2():
t.Log(ret)
default:
t.Log("default")
// 进行超时控制,在给定的时间内都没有返回,则运行这个case分支
case <-time.After(time.Millisecond * 1200):
t.Error("Time out,no return data was obtained")
}
}
输出如下。
=== RUN TestSelect
select_test.go:45: default
--- PASS: TestSelect (0.00s)
PASS
channel 的关闭
-
向关闭的 channel 发送数据,会导致 panic
-
v, ok <-ch; ok 为 bool 值,true 表示正常接受,false 表示通道关闭
-
所有的 channel 接收者都会在 channel 关闭时,立刻从阻塞等待中返回且上述 ok 值为 false。这个广播机制常被利用,进行向多个订阅者同时发送信号。如:退出信号。
如下,只有一个reveiver。
// 生产者
func dataProducer(ch chan int, wg *sync.WaitGroup) {
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
// 关闭 channel,close 操作只能由发送方执行,关闭以后不关闭接收方,只关闭发送方,不允许发送方再往通道中发送数据
// 关闭以后若channel的buffer中还有数据,接收方还可以继续去取
// 若接收方已经获取到了buffer中的最后一个元素,接收方再去取就会获得channel中定义元素类型的0值
close(ch)
// ch <- 11 往关闭的channel中发送数据,会出现 panic: send on closed channel
wg.Done()
}()
}
// 消费者
func dataReceiver(ch chan int, wg *sync.WaitGroup) {
go func() {
for {
//ok=true表示通道没有关闭
//如果不进行判断,channel已经关闭,
//但receiver还在从channel中取数据,不会陷入阻塞等待,会返回channel所定义类型的0值
if data, ok := <-ch; ok {
fmt.Println(data)
} else {
break
}
}
wg.Done()
}()
}
func TestChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
// 1个 receiver
wg.Add(1)
dataReceiver(ch, &wg)
wg.Wait()
}
输出
=== RUN TestChannel
0
1
2
3
4
5
6
7
8
9
--- PASS: TestChannel (0.00s)
PASS
增加多个receiver
func TestChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
// 多个 receiver
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Wait()
}
输出如下。虽然输出的顺序是乱的(因为有多个协程并发工作),但是数据是没有出现差错的,还是0 ~ 9。
=== RUN TestChannel
0
3
4
5
6
7
8
9
1
2
--- PASS: TestChannel (0.00s)
PASS
任务的取消
使用 channel 取消任务
方法1:通过向 channel 发送消息来取消任务。类似于使用 ctrl + c 发送信号量关闭程序一样。
func isCancelled(cancelChan chan struct{}) bool {
select {
// 如果能从cancelChan中接收到数据,则返回true,则表示受到了取消任务的消息
case <-cancelChan:
return true
default:
return false
}
}
// 通过发送消息的形式取消任务
func cancelWithMessage(cancelChan chan struct{}) {
cancelChan <- struct{}{}
}
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
cancelWithMessage(cancelChan)
time.Sleep(time.Millisecond * 5)
}
输出。从输出中发现只有一个任务被取消了,这符合预期的期望,因为只往 channel 中发送了一个数据(只调用了一次 cancelWithMessage(cancelChan)),只够一个消费者进行消费。需要注意的是,其它的任务都还没停止运行,但是因为主协程已经运行完毕了,所以程序就终止了。
=== RUN TestCancel
4 Cancelled
--- PASS: TestCancel (0.01s)
PASS
在示例中,有5个任务,需要让5个任务都被取消,就得执行5次 cancelWithMessage(cancelChan),如果在不知道任务数量的情况下,这种办法就没办法关闭所有任务了。
方法二:通过关闭 channel 来取消任务
func isCancelled(cancelChan chan struct{}) bool {
select {
// 如果能从cancelChan中接收到数据,则返回true
case <-cancelChan:
return true
default:
return false
}
}
// 通过关闭channel的形式取消任务
func cancelWithCloseChan(cancelChan chan struct{}) {
close(cancelChan)
}
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
cancelWithCloseChan(cancelChan)
time.Sleep(time.Millisecond * 5)
}
输出。从输出中可以发现所有的任务都被取消了。原理也比较简单,当发送方 close channel 后,此时 channel 的 buffer 中没有任何数据,接收方再从 channel 中获取数据时,就会得到一个 0值,这个0 究竟是什么类型得依赖于定义channel 时定义的数据类型。
=== RUN TestCancel
1 Cancelled
2 Cancelled
4 Cancelled
3 Cancelled
0 Cancelled
--- PASS: TestCancel (0.01s)
PASS
chan 传值还是传地址
需要注意的是,在 go 语言中只有值传递,也就是说传参数时会将参数值复制一份。但是在传递 chan 参数的时候,会发现不管是传值,还是传地址,其实都是 OK 的,都是复用的同一个通道。因为 chan是一个结构,这个结构在传递时是被复制的,其中的指针成员也会被复制到新的chan中,所以新旧两个chan会指向同一个内存区域。类似的还有slice和map。(这就类似于Java中的对象,Java也是值传递,传递对象时,不同的引用指向同一块内存区域,其中一个引用对该内存区域进行修改,另一个引用也能感知到)
第一段代码:传地址
func isCancelled(cancelChan *chan struct{}) bool {
select {
// 如果能从cancelChan中接收到数据,则返回true
case <-*cancelChan:
return true
default:
return false
}
}
// 通过发送消息的形式取消任务
func cancelWithMessage(cancelChan *chan struct{}) {
*cancelChan <- struct{}{}
}
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(&cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
cancelWithMessage(&cancelChan)
time.Sleep(time.Millisecond * 5)
}
第二段代码,传值
func isCancelled(cancelChan chan struct{}) bool {
select {
// 如果能从cancelChan中接收到数据,则返回true
case <-cancelChan:
return true
default:
return false
}
}
// 通过发送消息的形式取消任务
func cancelWithMessage(cancelChan chan struct{}) {
cancelChan <- struct{}{}
}
func TestCancel(t *testing.T) {
cancelChan := make(chan struct{}, 0)
for i := 0; i < 5; i++ {
go func(i int, cancelCh chan struct{}) {
for {
if isCancelled(cancelCh) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, cancelChan)
}
cancelWithMessage(cancelChan)
time.Sleep(time.Millisecond * 5)
}
上述两段代码的输出结果相同。
context 与任务取消
如下图所示,当任务与任务之间存在层级关系,我们取消叶子节点的任务相对来说比较容易,但是如果是取消中间某个节点,其实还比较麻烦,因为不仅要取消该任务,还要取消与之相关联的任务。
golang 1.9以后有了 context。可以通过 context 实现关联任务的取消。
-
根 Context:通过 context.Background () 创建
-
子 Context:通过 context.WithCancel(parentContext) 创建。示例:ctx, cancel := context.WithCancel(context.Background())。创建以后返回的 cancel 是一个方法,用来取消创建的这个 context。
-
当前 Context 被取消时,基于他的子 context 都会被取消
-
接收取消通知 <-ctx.Done()
话不多说,首先看第一段代码
func isCtxCancelled(ctx context.Context) bool {
select {
// 通过 ctx.Done 接收到取消的通知
case <-ctx.Done():
return true
default:
return false
}
}
func TestContextCancel(t *testing.T) {
// WithCancel方法根据父context创建一个副本,并返回一个cancel方法
//当调用该cancel方法时,与之关联(从其本身复制而得的context或者是其子context)的context都会被取消掉
ctx, cancel := context.WithCancel(context.Background())
//ctx1, _ := context.WithCancel(ctx)
for i := 0; i < 5; i++ {
go func(i int, ctx context.Context) {
for {
if isCtxCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
cancel()
// 等待其它协程执行完毕
time.Sleep(time.Millisecond * 100)
}
输出结果如下。调用了 cancel 方法后,所有的任务都被取消了。
=== RUN TestContextCancel
4 Cancelled
1 Cancelled
2 Cancelled
3 Cancelled
0 Cancelled
--- PASS: TestContextCancel (0.10s)
PASS
如下,再根据 context.Background() 创建而得的 context 再创建几个子 context:ctx1 和 ctx2。然后调用 ctx 的 cancel 方法。
func TestContextCancel(t *testing.T) {
// WithCancel方法根据父context创建一个副本,并返回一个cancel 方法
//当调用该cancel方法时,与之关联(从其本身复制而得的context或者是其子context)的context都会被取消掉
ctx, cancel := context.WithCancel(context.Background())
ctx1, _ := context.WithCancel(ctx)
ctx2, _ := context.WithCancel(ctx)
for i := 0; i < 5; i++ {
go func(i int, ctx context.Context) {
for {
if isCtxCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
for i := 5; i < 10; i++ {
go func(i int, ctx context.Context) {
for {
if isCtxCancelled(ctx1) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx1)
}
for i := 10; i < 15; i++ {
go func(i int, ctx context.Context) {
for {
if isCtxCancelled(ctx2) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx2)
}
cancel()
// 等待其它协程执行完毕
time.Sleep(time.Millisecond * 100)
}
输出结果如下。ctx 和其子context向关联的任务都被取消了。
=== RUN TestContextCancel
14 Cancelled
5 Cancelled
12 Cancelled
2 Cancelled
3 Cancelled
9 Cancelled
11 Cancelled
13 Cancelled
10 Cancelled
8 Cancelled
6 Cancelled
1 Cancelled
4 Cancelled
7 Cancelled
0 Cancelled
--- PASS: TestContextCancel (0.10s)
PASS
如下,根据 context.Background() 再创建几个节点,试试cancel能否取消这些任务。
func TestContextCancel(t *testing.T) {
// WithCancel方法根据父context创建一个副本,并返回一个cancel 方法
//当调用该cancel方法时,与之关联(从其本身复制而得的context或者是其子context)的context都会被取消掉
ctx, cancel := context.WithCancel(context.Background())
ctxBrother1, _ := context.WithCancel(context.Background())
for i := 0; i < 5; i++ {
go func(i int, ctx context.Context) {
for {
if isCtxCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctx)
}
for i := 10; i < 15; i++ {
go func(i int, ctx context.Context) {
for {
if isCtxCancelled(ctxBrother1) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(i, ctxBrother1)
}
cancel()
// 等待其它协程执行完毕
time.Sleep(time.Millisecond * 100)
}
输出如下。从输出中可以发现,cancel 只能取消该 context 以及其子context,不能作用于其 parent context,也不能作用其 brother context。
=== RUN TestContextCancel
3 Cancelled
1 Cancelled
4 Cancelled
0 Cancelled
2 Cancelled
--- PASS: TestContextCancel (0.10s)
PASS
cancel 的作用范围如下图所示。