内核级线程模型
用户线程与内核调度实体(KSE)是1:1关系,大部分编程语言的线程库都是对操作系统内核级线程的一层封装,创建出来的每个线程与一个不同的KSE静态关联,因此其调度完全由OS调度器来做。
优点:
充分利用CPU,实现真正的并行
缺点:
没创建一个用户级线程都系要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。
用户级线程模型
用户线程与KSE是多对1关系,一个进程中所有创建的线程都与一个KSE在运行时动态关联
许多语言的协程就是这种方式,上下文切换代价小,但容易出现调用阻塞的致命问题
优点:
上下文切换都发生在用户空间,避免的模态切换,从而对于性能有积极的影响
缺点:
同时只有一个处理器被利用,解决了并发问题,但是没有解决并行问题,如果线程因为I/O操作陷入了内核态,所有线程都将被阻塞
两极线程模型
Golng中的信号处理
系统信号处理主要涉及os包,os.signal包以及syscall包,其中最主要的函数是signal包中的Notify函数:
func Notify(c chan<-os.Signal, sig ...os.Signal)
该函数会将进程收到的系统信号发送给channel。
Kill pid 与kill -9 pid的区别
应用程序如何优雅退出
Go中的Signal发送和处理
对信号的处理主要使用os/signal包中的两个方法
notify 方法用来监听收到的信号
stop方法用来取消监听
监听全部信号:
package main
import (
"fmt"
"os"
"os/signal"
)
func main() {
c := make(chan os.Signal)
signal.Notify(c)
fmt.Println("启动")
s := <-c
fmt.Println("退出信号",s)
}
监听指定信号:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGUSR1, syscall.SIGUSR2)
fmt.Println("启动")
s := <-c
fmt.Println("退出信号",s)
}
优雅退出守护进程:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 创建监听退出chan
c := make(chan os.Signal)
// 监听指定信号
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
syscall.SIGQUIT)
// linux 下
//signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
// syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
for s := range c {
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
fmt.Println("退出", s)
ExitFunc()
//case syscall.SIGUSR1:
// fmt.Println("usr1", s)
//case syscall.SIGUSR2:
// fmt.Println("usr2", s)
default:
fmt.Println("other", s)
}
}
}()
fmt.Println("进程启动...")
sum := 0
for true {
sum++
fmt.Println("sum:", sum)
time.Sleep(time.Second)
}
}
func ExitFunc() {
fmt.Println("开始退出。。。")
fmt.Println("执行清理。。。")
fmt.Println("结束退出。。。")
os.Exit(0)
}
CSP(Communicating Sequential Process)顺序通信进程,一种并发编程模型,描述两个独立的并发实体通过共享的通讯channel(管道)进行通信,不关注发送消息的实体,而关注与发送消息时使用的channel.
Go通过goroutine与channel实现并发,goroutine类似协程coroutine,与coroutine的区别在于,能够在发现阻塞后启动新的微线程
Goroutine是实际并发执行的实体,底层使用协程coroutine实现并发
coroutine是一种运行在用户态的用户线程,具有以下优点:
Go并发调度:G-P-M模型, 可以让多核CPU中每个CPU执行一个协程:
goroutine实现了两级线程模型:G-P-M模型,该模型有4个重要结构M、G、P、Sched:
Processor的数量可以通过环境变量设置或者运行时设置,Processor数量固定意味着任意时刻只有对应数量的线程在执行代码
一个M对应一个P,一个P执行并维护一个goroutine队列,如果机器有多核并设置P为多个,同时就会有多个M:
线程阻塞
runqueue执行完成
Goroutine调度原理图
常用函数
NumCPU: 返回当前系统的CPU核数量
GOMAXPROCS: 设置最大可同时使用的CPU核数
package main
import (
"fmt"
"runtime"
)
func main() {
// 获取cpu的数量
fmt.Println("逻辑CPU的核数:", runtime.NumCPU())
// 设置go 程序执行的最大的:[1,256]
n := runtime.GOMAXPROCS(2)
fmt.Println(n)
}
Goshed:让当前线程让出cpu以让其他线程运行,它不会挂起当前线程,因为当前线程未来会继续执行
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("Goroutine...")
}
}()
for i := 0; i < 4; i++ {
runtime.Gosched()
fmt.Println("main..")
}
//Goroutine...
//Goroutine...
//Goroutine...
//Goroutine...
//Goroutine...
//main..
//main..
//main..
//main..
}
NumGoroutine :返回正在执行和排队的任务总数
Goexit: 终止协程
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
go func() {
fmt.Println("goroutine开始。。。")
fun()
fmt.Println("goroutine结束")
}()
time.Sleep(3*time.Second)
}
//goroutine开始。。。
//defer...
func fun(){
defer fmt.Println("defer...")
runtime.Goexit()
fmt.Println("func函数")
}
runtime.GC :会让运行时系统进行一次强制性的垃圾收集
GOROOT :获取goroot目录
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("GOROOT->", runtime.GOROOT())
fmt.Println("os/platform->", runtime.GOOS)
}
GOOS : 查看目标操作系统
goroutine是一个轻量级的线程,它的创建、调度都是在用户态进行,并不需要进入内核,这意味着开销是很小的所以,一般使用goroutine是不会考虑使用协程池的
对于一些生产者消费者模型,就需要使用到协程池的实现:
例如:随机生成数字,计算数字各个位数相加之和:
package main
import (
"fmt"
"math/rand"
)
type Job struct {
Id int
RandNum int
}
type Result struct {
job *Job
sum int
}
func main() {
// 需要2个管道
// 1.job管道
jobChan := make(chan *Job, 128)
// 2. 结果管道
resultChan := make(chan *Result, 128)
// 3. 创建工作池
createPool(64, jobChan, resultChan)
// 4. 开个打印的协程
go func(resultChan chan *Result) {
for result := range resultChan {
fmt.Printf("job id %v random:%v result:%d\n", result.job.Id,
result.job.RandNum, result.sum)
}
}(resultChan)
var id int
// 循环创建job, 输入到管道
for {
id++
// 生成随机数
r_num := rand.Int()
job := &Job{
Id: id,
RandNum: r_num,
}
jobChan <- job
}
}
// 创建工作池
// num: 开几个协程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
for i := 0; i < num; i++ {
go func(jobChan chan *Job, resultChan chan *Result) {
//遍历job 管道所有数据,进行相加
for job := range jobChan {
r_num := job.RandNum
var sum int
for r_num != 0 {
tmp := r_num % 10
sum += tmp
r_num /= 10
}
r := &Result{
job: job,
sum: sum,
}
resultChan <- r
}
}(jobChan, resultChan)
}
}
不断创建goroutine而不去关闭,会导致内存溢出的问题,我们在使用goroutine的时候一定要知道什么时候能够退出和控制goroutine
使用goroutine导致内存溢出的案例:
package main
import (
"fmt"
"runtime"
)
// 模仿耗时的业务代码
func consumer(ch chan int) {
for {
data := <- ch
fmt.Println(data)
}
}
func main() {
// 用于goroutine通信
ch := make(chan int)
for {
var dummy string
// 获取输入,模拟进程持续运行
fmt.Scan(&dummy)
// 使用goroutine运行耗时业务代码
go consumer(ch)
// 输出现在的goroutine数量
fmt.Println("goroutines:", runtime.NumGoroutine())
}
}
正确写法需要指定退出goroutine的时机:
package main
import (
"fmt"
"runtime"
)
// 模仿耗时的业务代码
func consumer(ch chan int) {
for {
data := <- ch
if data == 0 {
break
}
fmt.Println(data)
}
fmt.Println("goroutine exit")
}
func main() {
// 用于goroutine通信
ch := make(chan int)
for {
var dummy string
// 获取输入,模拟进程持续运行
fmt.Scan(&dummy)
if dummy == "quit" {
// 设置子 goroutine退出标志
for i := 0; i < runtime.NumGoroutine()-1; i++ {
ch <- 0
}
}
// 使用goroutine运行耗时业务代码
go consumer(ch)
// 输出现在的goroutine数量
fmt.Println("goroutines:", runtime.NumGoroutine())
}
}
goroutine通过channel实现通信,可以把channel理解为一个通信队列
channel是引用类型,会被垃圾回收机制回收,空值为nil, 只有通过make初始化的channel才可以使用
无缓冲通道:
make(chan interface{})
有缓冲的通道:
make(chan interface{} 2)
关闭channel:
for range
遍历获取channel中的值,或者x, ok := <- channel
从channel中取值时第二个参数可以判断channel是否关闭单向通道:
channel作为函数参数传参格式chan <- interface{}
:只接收值的channel
<- chan interface{}
:只写入值的channel
作为参数传递为引用类型
一般通过单向通道实现生产者消费者模型:
package main
import "fmt"
func producer(out chan <- int) {
for i := 0; i < 10; i++ {
out <- i * i
}
close(out)
}
func consumer(in <- chan int) {
for num := range in {
fmt.Println("num=", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
channel超时机制
当channel中没有值的时候,再从channel中获取值会阻塞,可以通过select
加上单向通道技巧实现channel超时机制:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case num := <-ch:
fmt.Println("num =", num)
case <- time.After(3*time.Second):
fmt.Println("超时")
quit <- true
}
}
}()
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
<- quit
fmt.Println("程序结束")
}
channel 可以声明为只读,或者只写性质
package main
import "fmt"
func main() {
var ch chan int
ch = make(chan int, 10)
exitChan := make(chan struct{}, 2)
go send(ch, exitChan)
go recv(ch, exitChan)
var total = 0
for _ = range exitChan {
total++
if total == 2 {
break
}
}
fmt.Println("结束。。。")
}
func send(ch chan <- int, exitChan chan struct{}) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
var a struct{}
exitChan <- a
}
func recv(ch <- chan int, exitChan chan struct{}) {
for {
v, ok := <- ch
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitChan <- a
}
使用select 可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
"time"
)
func main() {
// 1. 定义一个管道10 个int数据
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
// 2. 定义一个管道5个string 数据
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
// 传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock
// 在实际开发中很多情况我们并不能确定什么时候需要关闭channel
// 就可以使用select 方式解决channel关闭问题
for {
select {
case v := <- intChan:
fmt.Println(v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Println(v)
time.Sleep(time.Second)
default:
fmt.Println("over!")
time.Sleep(time.Second)
return
}
}
}
goroutine 中使用recover, 解决协程中出现panic导致程序崩溃问题
package main
import (
"fmt"
"time"
)
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello, world")
}
}
func test() {
// 使用defer + recover
defer func() {
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
// 定义一个nil的map, 抛出异常
var wrongMap map[int]string
wrongMap[0] = "golang"
}
channel 发送和接收元素的本质是:值的拷贝
package main
import (
"fmt"
"time"
)
type user struct {
name string
age int8
}
var u = user{name: "Ankur", age: 25}
var g = &u
func modifyUser(pu *user) {
fmt.Println("modifyUser Received Value", pu)
pu.name = "Anand"
}
func printUser(u <- chan *user) {
time.Sleep(2 * time.Second)
fmt.Println("printUser goRoutine called", <- u)
}
func main() {
c := make(chan *user, 2)
// g 是指向u的地址,将g 发送至channel,就是将g的值(指向u的地址)copy发送
c <- g
fmt.Println(g)
// 修改g指向的u的地址,g的值改变了
g = &user{
name: "Ankur annad",
age: 100,
}
// 从channel中取出g的值并不受上边的改变而改变
// printUser goRoutine called &{Ankur 25}
go printUser(c)
// 不使用channel, 直接通过参数传递g, 然后修改g的值
//modifyUser Received Value &{Ankur annad 100}
go modifyUser(g)
time.Sleep(5 * time.Second)
// g的值被改变了
// &{Anand 100}
fmt.Println(g)
}
channel 可能会引发goroutine 泄露:goroutine 操作 channel 后,处于发送或接收阻塞状态,而 channel
处于满或空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而
导致 gouroutine 会一直处于等待队列中
停止信号:channel 结合time 可以实现超时控制,与定时执行:
select {
// 100 ms后还没有从s.stopc获取数据就超时退出
case <-time.After(100 * time.Millisecond):
case <-s.stopc:
return false
}
// 没隔一秒执行定时任务
func worker() {
ticker := time.Tick(1 * time.Second)
for {
select {
case <-ticker:
// 执行定时任务
fmt.Println("执行1s定时任务")
}
}
}
解耦生产方和消费方:
package main
import (
"fmt"
"time"
)
func main() {
taskCh := make(chan int, 100)
go worker(taskCh)
// 往任务channel中添加任务
for i := 0; i < 10; i++ {
taskCh <- i
}
// 阻塞主线程
select {
case <-time.After(time.Hour):
}
}
func worker(taskChan <- chan int) {
const N = 5
// 启动5个工作协程
for i := 0; i < N; i++ {
go func(id int) {
for {
task := <- taskChan
fmt.Printf("finish task: %d by worker %d\n", task, id)
time.Sleep(time.Second)
}
}(i)
}
}
控制并发数:
var limit = make(chan int, 3)
func main() {
// …………
for _, w := range work {
go func() {
limit <- 1
w()
// 如果上边的w()逻辑发生异常,将不会从limit 中取数据,需要结合defer 取出数据
<-limit
}()
}
// …………
}
需要注意limit 要放在goroutine的内部:
如果放在外层,就是控制goroutine的数量,可能会阻塞for 循环,影响业务逻辑。
定时器有两种:
Timer
: 一次性的时间触发事件
Timer 三要素:
定时时间:d
触发动作:f
时间channel: t
常见的创建方式:
t := time.NewTimer(d)
t := time.AfterFunc(d, f)
c := time.After(d)
Ticker
: 按一定时间间隔持续触发时间事件
time.NewTimer()
package main
import (
"fmt"
"time"
)
func main() {
// 新建一个计时器
timer := time.NewTimer(3 * time.Second)
fmt.Printf("%T\n", timer)
fmt.Println(time.Now())
// 将等待channel 中的信号, 阻塞三秒
ch2 := timer.C
fmt.Println(<-ch2)
}
timer.Stop
package main
import (
"fmt"
"time"
)
func main() {
// 新建一个计时器
timer := time.NewTimer(5 * time.Second)
// 在另外的线程中触发计时器
go func() {
<-timer.C
fmt.Println("Timer结束")
}()
// 由于上边的计时器是在另外的线程中触发,主线程的代码将继续执行
time.Sleep(3*time.Second)
//sleep 3 秒后timer 并没有结束, 使用stop 停止计时器
stop := timer.Stop()
if stop {
fmt.Println("timer 停止")
}
}
time.After()
package main
import (
"fmt"
"time"
)
func main() {
ch1 := time.After(3 * time.Second)
fmt.Printf("%T\n", ch1)
fmt.Println(time.Now())
time2 := <- ch1
fmt.Println(time2)
}
在使用channel的时候,通常无法获知channel是否关闭,关闭一个closed的channel 会导致panic, 向一个closed的channel中发送数据也会导致panic
优雅关闭channel方案:发送者关闭channel
发送者关闭channel 分为4中情况:
一个sender, 一个receiver, 发送者发送完毕直接关闭
一个sender, M 个receiver,发送者发送完毕直接关闭
N 个 sender, 一个receiver,增加一个传递关闭信号的channel, receiver 通过信号channel 下达关闭数据channel指令。sender 监听到关闭信号后,停止发送数据。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 100000
const NumSenders = 1000
dataChan := make(chan int, 100)
stopChan := make(chan struct{})
// sender
for i := 0; i < NumSenders; i++ {
go func() {
select {
case <-stopChan:
// sender 接收到关闭dataChan 的信号,没有主动关闭channel,而是直接return
// 当channel没有被goroutine 使用时会被gc回收
// 此处优雅的体现了让gc回收channel
return
case dataChan <- rand.Intn(Max):
}
}()
}
// receiver
go func() {
for value := range dataChan {
if value == Max-1 {
fmt.Println("send stop signal to senders.")
close(stopChan)
return
}
fmt.Println(value)
}
}()
select {
case <- time.After(time.Hour):
}
}
N 个 sender, M个receiver, 同样需要一个传递关闭信号的channel, 但由于多个receiver 信号channel 不能由receiver 直接关闭,否则会panic
此时需要引入一个中间人,M个receiver 都会向它发送关闭dataChan的“请求”, 中间人收到第一个请求后,就会直接下达关闭dataChan的指令, N个sender 也可以向中间人发送关闭dataChan的指令。
package main
import (
"fmt"
"math/rand"
"strconv"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 100000
const NumReceivers = 10
const NumSenders = 1000
dataChan := make(chan int, 100)
stopChan := make(chan struct{})
// toStop: 就是向中间人发送请求的channel, 必须为带缓存的channel,
// 否则直接向中间人channel发送消息会导致panic异常
toStopChan := make(chan string, 1)
var stoppedBy string
// 中间人
go func() {
stoppedBy = <- toStopChan
fmt.Println(stoppedBy)
close(stopChan)
}()
// 发送者
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
select {
case toStopChan <- "sender#" + id:
default: // 防止向toStopChan写入阻塞加上default
}
return
}
select {
case <-stopChan:
return
case dataChan <- value:
}
}
}(strconv.Itoa(i))
}
// 接收者
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
case <-stopChan:
return
case value := <- dataChan:
if value == Max - 1 {
select {
case toStopChan <- "receiver#" + id + "#" + strconv.Itoa(value):
default: // 防止向toStopChan写入阻塞加上default
}
return
}
//fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <-time.After(time.Hour):
}
}
死锁
多个线程因争夺资源造成互相等待的现象,若无外力将处于死锁状态
发生死锁的条件:
互斥条件:
线程对资源的访问是排他性的,如果一个线程占用了资源,其他线程都等待该线程释放资源
请求和保持条件:
线程T1已经占用了资源R1,但又提出使用资源R2请求,但是R2已经被其他线程占用,导致T1也必须等待,但又对自己保持的资源R1不释放
不剥夺条件:
线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放
环路等待条件:
即:{p0,p1,p2,…pn},进程 p0(或线程)等待 p1 占用的资源,p1 等待 p2 占用的资源,pn 等待 p0 占用的资源。
解决方案:
活锁
饥饿
go build
命令中多加了一个-race
标志,这样生成的可执行程序自带了检测资源竞争的功能原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针
atomic.AddInt64
多个goroutine间安全的加同一个变量
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait() // 等待goroutine结束
fmt.Println(counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1) // 安全的对counter 加1
runtime.Gosched() // 暂停当前goroutine, 退回执行队列,让其他等待的goroutine 执行
}
}
LoadInt64
和StoreInt64
。这两个函数提供了一种安全地读和写一个整型值的方式
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var (
shutdown int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go doWork("A")
go doWork("B")
time.Sleep(1 * time.Second)
fmt.Println("Shutdown Now")
atomic.StoreInt64(&shutdown, 1)
wg.Wait()
}
func doWork(name string) {
defer wg.Done()
for {
fmt.Printf("Done %s Work\n", name)
time.Sleep(250 * time.Millisecond)
if atomic.LoadInt64(&shutdown) == 1 {
fmt.Printf("shutting %s Down\n", name)
break
}
}
}
Done B Work
Done A Work
Done A Work
Done B Work
Done B Work
Done A Work
Done A Work
Done B Work
Shutdown Now
shutting A Down
shutting B Down
互斥锁
使用互斥锁也可以实现资源共享
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int64
wg sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println(counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
//同一时刻只允许一个goroutine进入这个临界区
mutex.Lock()
{
value := counter
runtime.Gosched()
value++
counter = value
}
mutex.Unlock() //释放锁,允许其他正在等待的goroutine进入临界区
}
}
sync.WaitGroup
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) |
计数器+delta |
(wg *WaitGroup) Done() |
计数器-1 |
(wg *WaitGroup) Wait() |
阻塞直到计数器变为0 |
sync.Once
sync.Once
只有一个Do方法:func (o *Once) Do(f func()) {}
如果函数f 需要传递参数就需要使用闭包sync.Map
package main
import (
"fmt"
"strconv"
"sync"
)
//var m = make(map[string]int) go语言中的map是线程不安全的
var m = sync.Map{} // 开箱即用无需make
func get(key string) int {
value, _ := m.Load(key)
return value.(int)
}
func set(key string, value int) {
m.Store(key, value)
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v, v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
context
上下文用于协程间信息交互,方法如下:
Deadline
: 返回当前context 被取消的时间,也就是完成工作的截止时间Done
: 返回一个channel,该channel在当前工作完成或者上下文被取消后关闭,多次调用Done会返回同一个channelErr
: 返回当前Context结束的原因,只会在Done返回的channel被关闭时才会返回非空的值:
value
: 从context 中获取对应键的值WithCancel
控制单个协程
package main
import (
"context"
"fmt"
"time"
)
func reqTask(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println("stop", name)
return
default:
fmt.Println(name, "send request")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go reqTask(ctx, "worker1")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(3 * time.Second)
}
控制多个协程
func main() {
ctx, cancel := context.WithCancel(context.Background())
go reqTask(ctx, "worker1")
go reqTask(ctx, "worker2")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(3 * time.Second)
}
为每个子协程传递同一个ctx即可
WithDeadline
带有过期时间的上下文:
package main
import (
"context"
"fmt"
"time"
)
func main() {
d := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
// ctx 可能会过期但在任何情况下都最好调用cancel函数
defer cancel()
select {
case <-time.After(1*time.Second): // 当ctx 没过期时1秒后退出
fmt.Println("overslept")
case <-ctx.Done(): // 打印ctx 过期退出的错误信息
fmt.Println(ctx.Err())
}
}
当过期时,或者当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回通道的上下文将被关闭
WithTimeout
WithValue
上下文key, value
package main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
}
- **控制多个协程**
```go
func main() {
ctx, cancel := context.WithCancel(context.Background())
go reqTask(ctx, "worker1")
go reqTask(ctx, "worker2")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(3 * time.Second)
}
为每个子协程传递同一个ctx即可
WithDeadline
带有过期时间的上下文:
package main
import (
"context"
"fmt"
"time"
)
func main() {
d := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
// ctx 可能会过期但在任何情况下都最好调用cancel函数
defer cancel()
select {
case <-time.After(1*time.Second): // 当ctx 没过期时1秒后退出
fmt.Println("overslept")
case <-ctx.Done(): // 打印ctx 过期退出的错误信息
fmt.Println(ctx.Err())
}
}
当过期时,或者当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回通道的上下文将被关闭
WithTimeout
WithValue
上下文key, value
package main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}