go语言学习之并发编程

协程

早在接触 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 resultotherTask 结束之前就已经执行了,接下来的步骤就是阻塞在 retCh <- ret 这里,Channel 中需要阻塞等待发送方准备好数据,然后等待接收方获取数据,才能继续往下执行。上面的输出结果是最后打印 AsyncService is done 的,表明了在
TestAsyncServicefmt.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:ctx1ctx2。然后调用 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 的作用范围如下图所示。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值