【求职之路1-3】Go的并发编程

在开始 Go 语言并发编程之前,我们先简单地了解一些概念:

  • 1、进程线程
    • 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
    • 线程是进程的一个执行实例,是 CPU 调度和分派的基本单位。
    • 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
  • 2、协程线程
    • 协程,独立的栈空间,共享堆空间,调度由用户自己控制。
    • 线程,在一个线程上可以跑多个协程,就是说协程是轻量级的线程。
  • 3、并发并行
    • 多线程程序在一个核心的 CPU 上运行,就是并发,并发主要由切换时间片来实现"同时"运行。

    • 多线程程序在多个核心的 CPU 上运行,就是并行,并行是直接利用多核实现多线程的运行。

Go 语言中的并发其实就是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine 时,Go 会将其视为一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。

1.goroutine

golang runtime GPM 调度机制

调度器历史

(1)单进程时代
早期的操作系统,每个程序就是一个进程,只有当一个程序运行完毕才能进行下一个进程。当然这样的系统并不高效,我们都直到它的缺点:

  • 只能一个一个任务处理,速度很慢;
  • 如果有进程被阻塞在读磁盘文件的时候,CPU 就完全空闲,而后面的任务只能干等。

(2)多进程/线程时代
在多进程/线程的操作系统中,把每个任务都切成好几片,用一个 CPU 调度器去管理。这样做的好处是,当其中一个进程阻塞的时候,我们可以安排其他进行去执行,这样我们的 CPU ‘帕鲁’ 就能不间断地干活了。

但是,聪明的我们又发现问题了:每个进程的创建、切换、销毁等等也是占用时间的,要是任务特别多的话,CPU 就又要花很多时间在处理进程的调度,并没有在正在地干活。

(3)协程时代
线程其实分有“内核态线程”和“用户态线程”,而且一个“用户态线程”必须要绑定一个“内核态线程”。

话是这么说的,但实际上 CPU 并不真知道“用户态线程”,它只明白它要运行的是一个“内核态线程”,在 Linux 中就是一个进程控制模块,PCB。

于是,我们就想能不能让多个“用户态线程”绑定一个“内核态线程”呢?

所以,我们就有了协程(co-routine),协程就是“用户态线程”,而线程(thread) 就是“内核态线程”。

协程通过协程调度器给到线程,而线程通过 CPU 调度器给到 CPU 去执行。协程的调度是协作式的,就是只有当一个协程选择放弃对 CPU 的控制时,才会被切换;而线程的调度是抢占式的,如果有一个线程比当前执行的线程优先级高,那么当前线程就会被掐断,要乖乖把 CPU 让给优先级高的线程去执行。

这里可以根据协程与线程的映射关系,可以分为:N对1、1对1、N对M。

N对1的情况:N个协程与1个线程绑定,好处是协程之间的切换不用切换到内核态去,缺点就是阻塞后其他协程只能等待。

1对1的情况:实际就像是线程的执行,完全没有协程的优势了。

N对M的情况:这种情况是比较好的,但是怎么去调度就是一个问题了。

Goroutine 调度器

现在,我们对协程、线程和调度器有一定的认识了。接下来,在学习 Go 的协程调度之前,我们先看看线程池

线程池是这样设计的,我们把每个 任务G(通常是个函数,注意这不是协程)发布到任务队列中,然后让线程池中的 线程woker 去把任务从队列中取出执行,当然 线程woker 的调度是由操作系统去做的。

通过线程池,我们规定只创建一定数量的线程,每个线程一次携带一个任务去执行,执行完后再回来携带另一个任务。

因为线程是固定数量的,要是这些线程都被阻塞,那么整个任务队列就会停摆。当然我们也可以增加线程池中的线程数量,但是数量多的话,就会由很多个线程去争抢 CPU,反而不一定能提高消费能力。


那么,我们看一下 Go 是怎么做的吧。首先我们要了解一些概念:

  • G(Goroutine):就是 Go 协程,每个 go 关键字都会创建一个协程。
  • M(Machine):工作线程,在 Go 中称为 Machine。
  • P(Processor):逻辑处理器,包含运行 Go 代码的必要资源,也由调度 goroutine 的能力。

规则是:每个工作线程(M)需要协程(G)才能执行,而且逻辑处理器§有一个包含多个协程(G)的队列,然后逻辑处理器§可以调度协程(G)给工作线程(M)执行。

逻辑处理器§的个数是在程序启动时决定的,默认情况下等同于 CPU 的核数,而且一个逻辑处理器§只绑定一个工作线程(M),所以线程个数默认等同于 CPU 的个数。

Goroutine 调度策略

我们知道了 Goroutine 的调度是通过逻辑处理器§去调度的,那么具体是怎么做的呢?

队列轮转
在逻辑处理器§自己维护的队列上,如果协程(G)没有被阻塞,那么就将其执行一段时间后终止,将上下文保存下来后放到队列的尾部,然后从队列中重新取出一个协程(G)进行调度。

除了每个逻辑处理器§上的协程(G)队列外,运行时还有一个全局的队列。全局的队列是用来收留那些从系统调用中回复的协程(G)的。

所以,每个逻辑处理器§除了要调度自己的队列外,还要看看全局队列中是否有协程(G),有的话就把它调度到自己维护的队列上。

系统调用
那么发生系统中断的时候会发生的事情就是,逻辑处理器§会将当前这个工作线程(M)与当前执行的协程(G)脱离。

脱离的线程(M)就在一遍等待系统的调用,而逻辑处理器§会新建立一个线程,继续执行它自己维护队列里的协程(G)。

工作量窃取
当某个逻辑处理器§自己队列上的协程(G)都处理完了后,除了去看全局队列,它还会去看别的逻辑处理器§是否把它们的协程(G)都处理完了。

如果发现其他逻辑处理器§的队列上还有协程(G),那么它就会窃取一部分协程(G)过来执行,一般每次偷取一半。

一般来讲,程序运行时就将 GOMAXPROCS 大小设置为 CPU 核数,可让 Go 程序充分利用 CPU。在某些 IO 密集型的应用里,这个值可能并不意味着性能最好。理论上当某个 Goroutine 进入系统调用时,会有一个新的 M 被启用或创建,继续占满 CPU 、。但由于 Go 调度器检测到 M 被阻塞是有一定延迟的,也即旧的 M 被阻塞和新的 M 得到运行之间是有一定间隔的,所以在 IO 密集型应用中不妨把 GOMAXPROCS 设置的大一些,或许会有好的效果。

goroutine 同步与通信

Go 的运行时(runtime)是 Go 的核心部分,负责管理内存分配、垃圾回收、并发调度等底层任务。

我们的协程 goroutine 是由运行时管理的,就连 main 函数也都是运行在 goroutine 上的。

package main

import (
	"fmt"
	"time"
)

func test(){
	fmt.Println("I am work in a single goroutine")
}

func main(){
	// 把函数 test() 给到 goroutine
	go test()
	// 在 main 的 goroutine 上休眠一下
	time.Sleep(time.Second)
}

上面的代码,假设我们不进行休眠的话,那么 main 执行完 go test() 就会马上结束,而 go test() 可能还没开始执行,所以可能什么都没返回。

此外,和其他编程语言一样,Go 不同 goroutine 间的代码次序并不能代表真正的执行顺序。

package main

import "fmt"

func setVTo1(v *int){
	*v = 1
}

func setVTo2(v *int){
	*v = 2
}

func main(){
	v := new(int)
	go setVTo1(v)
	go setVTo2(v)
	fmt.Println(*v)
}

上面代码的执行结果可能为 0、1或2,但最有可能的情况是 0,这取决于调度器的调度情况。

为了能够按照我们想要的顺序去执行 goroutine ,就需要用到 同步与通信 的一些方法去执行并发:

  • channel:提供一种安全且同步的方式来发送和接收值。
  • select:用于同时等待多个 channel 操作,处理第一个可用的 channel
  • sync.WaitGroup:提供一种简单的方法来等待一组 goroutine 完成执行。
  • sync.Mutex:提供了一种锁机制来保护共享资源,防止并发访问导致的数据竞争。
  • sync.RWMutext:读写锁,允许多个读操作同时进行,但写操作需要独占锁。

2.channel

Go 语言中倡导用 channel 作为 goroutine 之间同步和通信的手段。

channel 类型属于引用类型,而且每个 channel 只能传递固定类型的数据。

// 声明 channel 并指定传递类型为 T
var channelName chan T

// make 初始化 channel,sizeOfChan 为缓冲
ch := make(chan T, sizeOfChan)

其中,默认不带缓冲的 channel 也叫 同步 channel,收发数据的操作都会被阻塞;带缓冲的 channel 叫 异步 channel 收发数据时,如果 channel 未达到阻塞条件则不会被阻塞。

channel 的发送与接收

channel 作为一个队列,它总是保证数据收发顺序总是遵循先入先出的原则,同时也保证同一时刻只有一个 goroutine 访问 channel 来发送和获取数据。

此外,如果不用 defer 语句关闭 channel 或者用 sync.WaitGroupDone 方法关闭 goroutine 的话,可能会导致 goroutine 泄露。

channel <- val 表示数据 val 将被发送到 channel 中;如果 channel 被填满之后,再向通道发送数据,则会阻塞当前的 goroutine。

val := <- channel 表示从 channel 中读取值并赋值到 val;如果 channel 没有数据,那么就阻塞要读取数据的 goroutine 直到有数据进入 channel。

如果想在读 channel 时返回,可以写成 val, ok := <- channel 只要检查一下 ok 就能判断是否读到了有效数据。

package main

import (
	"bufio"
	"fmt"
	"os"
)

func printInput(ch chan string){
	// for 循环从 channel 中读取数据
	for val := range ch{
		if val == "EOF"{
			break
		}
		fmt.Printf("Input is %s \n", val)
	}
}

func main(){
	// 无缓冲 channel
	ch := make(chan string)
	// 启动协程,因为 chan 无数据先被阻塞
	go printInput(ch)

	// 从命令行读取输入发给 goroutine
	scanner := bufio.NewScanner(os.Stdin)
	// 阻塞,等待命令行输入
	for scanner.Scan(){
		val := scanner.Text()
		ch <- val
		if val == "EOF"{
			fmt.Println("End the game!")
			break
		}
	}
	// 最后关闭 chan 
	defer close(ch)
}

带缓冲的 channel

如果创建 channel 时指定了 channel 长度,那么 channel 就会拥有缓冲区,goroutine 在缓冲区未填满时往 channel 发送数据将不会被阻塞。

package main

import (
	"fmt"
	"time"
)

func consume(ch chan int){
	// 线程休息 3s 后再开始读数据
	time.Sleep(time.Second * 3)
	<- ch
}

func main(){
	// 长度为 2 的 channel
	ch := make(chan int, 2)
	// 启动线程
	go consume(ch)

	ch <- 0
	ch <- 1
	fmt.Println("I am free!")
	ch <- 2  // 阻塞,等待 goroutine 读取数据
	fmt.Println("I can not go there within 3s!")
	time.Sleep(time.Second)
}

此外,我们可以声明单向的 channel,就是只能从 channel 发送数据或者只能从 channel 中读取数据,一般用于在传递 channel 时作为函数的参数,保证代码接口的严谨,避免进行越界操作。

ch := make(chan T)

// 声明只能发送的通道
var chInputOnly chan <- int = ch

// 声明只能接收的通道
var chOutputOnly int <- chan = ch

select 与 channel

如果 goroutine 中需要接收多个 channel 中的消息时,可以使用 Go 语言中的 select 提供的多路复用功能。

select 的使用方式与 switch 类似,但要求 case 语句后面必须为 I/O 操作,在没有 I/O 响应且没有提供 default 语句时,goroutine 会被阻塞。

package main

import (
	"fmt"
	"time"
)

func send(ch chan int, begin int){
	for i := begin; i < begin + 10; i++{
		ch <- i
	}
}

func main(){
	ch1 := make(chan int)
	ch2 := make(chan int)

	go send(ch1, 0)
	go send(ch2, 10)

	// 休眠,保证管道有数据
	time.Sleep(time.Second)

	for {
		select{
			case val := <- ch1:
				fmt.Printf("Get value %d from ch1\n", val)
			case val := <- ch2:
				fmt.Printf("Get value %d from ch2\n", val)
			// 超时设置
			case <-time.After(2*time.Second):
				fmt.Println("Time out")
				return 
		}
	}
}

我们用 select 多路复用分别从 ch1 和 ch2 中读取数据,如果多个 case 语句中的 ch 同时到达,那么 select 会运行一个伪随机算法随机选择一个 case。

因为 channel 的阻塞时无法被中断的,因此通过超时设置来去中断返回是一个好用的技巧。

此外,要注意在使用 channel 时可能会发生的思索情况:

  • 1、相互等待:两个或多个 goroutine 相互等待对方发送或接收数据,但都没有继续执行的操作。
  • 2、向已关闭的 channel 发送数据:尝试向一个已经被关闭的 channel 发送数据,会导致运行时 panic。
  • 3、所有 goroutine 都在等待:所有 goroutine 都在等待从某个 channel 接收数据,而没有其他 goroutine 会向这个 channel 发送数据。

3.sync

sync 提供了如下 7 种并发工具:

并发工具说明
Mutex互斥锁
RWMutex读写锁
WaitGroup并发等待组
Map并发安全字典
Cond同步等待条件
Once只执行一次
Pool临时对象池
Atomic原子操作

Mutex(互斥锁)

sync.Mutex 能够保证在同一个时间段内仅有一个 goroutine 持有锁,这就能保证在某个时间段内有且仅有一个 goroutine 访问共享资源,其他申请锁的 goroutine 将会被阻塞直到锁被释放。

// sync.Mutex 提供两种方法
Lock()
Unlock()
package main

import (
	"fmt"
	"sync"
	"time"
)

func main(){
	var lock sync.Mutex

	go func(){
		// 加锁
		lock.Lock()
		defer lock.Unlock()
		fmt.Println("FUNC1 get lock at " + time.Now().String())
		time.Sleep(time.Second)
		fmt.Println("FUNC1 release lock " + time.Now().String())
	}()

	time.Sleep(time.Second / 10)

	go func(){
		// 加锁
		lock.Lock()
		defer lock.Unlock()
		fmt.Println("FUNC2 get lock at " + time.Now().String())
		time.Sleep(time.Second)
		fmt.Println("FUNC2 release lock " + time.Now().String())
	}()

	// 等待所有 goroutine 执行完
	time.Sleep(time.Second * 4)
}

RWMutex(读写锁)

sync.RWMutex 将读锁和写锁的操作分离开来,为了保证在同一时间段内能够有多个 goroutine 访问同一资源,它要满足一下条件:

  • 同一时段,只能有一个 goroutine 获取写锁。
  • 同一时段,可以有多个 goroutine 获取读锁。
  • 同一时段,要么都是写锁,要么都是读锁(互斥)。

概括起来就是,只允许多读或单写。

// sync.RWMutex 提供以下接口
// 写加锁
func (rw *RWMutex) Lock()  

// 写解锁
func (rw *RWMutex) Unlock()

// 读写锁
func (rw *RWMutex) RLock()

// 读解锁
func (rw *RWMutex) RUnlock()

下面代码运行之后,我们会发现所有的 read goroutine 可以同时申请到读锁,而申请写锁的 goroutine 则必须等到锁被释放后才能争抢。

package main

import (
	"fmt"
	"strconv"
	"sync"
	"time"
)

var rwLock sync.RWMutex

func main(){
	// 获取读锁
	for i := 0; i < 5; i++{
		go func(i int){
			rwLock.RLocker()
			defer rwLock.RLocker()

			fmt.Println("read func " + strconv.Itoa(i) + " get rlock at " + time.Now().String())
			time.Sleep(time.Second)
		}(i)
	}

	time.Sleep(time.Second / 10)
	
	// 获取写锁
	for i := 0; i < 5; i++{
		go func(i int){
			rwLock.Lock()
			defer rwLock.Unlock()

			fmt.Println("write func " + strconv.Itoa(i) + " get rlock at " + time.Now().String())
			time.Sleep(time.Second)
		}(i)
	} 
	// 等待所有 goroutine 执行完
	time.Sleep(time.Second * 4)
}

WaitGroup(并发等待组)

sync.WaitGroup 的 goroutine 会等待预设好数量的 goroutine 都执行提交结束后,才会继续往下执行代码。

// sync.WaitGroup 提供的接口
// 添加等待数量,传递负数表示任务减一
func (wg *WaitGroup) Add(delta int)

// 等待数量数量减一
func (wg *WaitGroup) Done()

// 让 goroutine 等待在这里
func (wg *WaitGroup) Wait()

在 goroutine 调用 waitGroup.Wait 进行等待之前,要保证 waitGroup 中等待数量大于 1,即 waitGroup.Add 方法需要在 waitGroup.Wait 之前执行,否则等待就会被忽略,此外需要保证 waitGroup.Done 执行次数与 waitGroup.Add 添加的等待数量一致,过少会导致等待 goroutine 死锁,过多会导致程序 panic。

package main

import (
	"fmt"
	"strconv"
	"sync"
	"time"
)

func main(){
	var waitGroup sync.WaitGroup

	// 添加等待 goroutine 数量为 5
	waitGroup.Add(5)

	for i := 0; i < 5; i++{
		go func(i int){
			fmt.Println("work " + strconv.Itoa(i) + " is done at " + time.Now().String())
			time.Sleep(time.Second)
			waitGroup.Done()
		}(i)
	}
	waitGroup.Wait()
	fmt.Println("all works are done at " + time.Now().String())
}

Map(并发安全字典)

sync.Map 就是添加了同步控制的字典,Go 原生的字典并不是并发安全的,当多个 goroutine 同时往 Map 中添加数据时,可能会导致部分添加的数据丢失。

// sync.Map 提供以下接口
// 根据 key 获取存储值
func (m *Map) Load(keu interface{}) (value interface{}. ok bool)

// 设置 key_value 对
func (m *Map) Store(key, value interface{})

// 如果 key 存在则返回 key 对应的 value,否则设置 key-value 对
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)

// 删除一个 key 以及对应的值
func (m *Map) Delete(key interface{})

// 无序遍历 map
func (m *Map) Range(f func(key, value interface{}) bool)
package main

import (
	"fmt"
	"strconv"
	"sync"
)

var syncMap sync.Map
var waitGroup sync.WaitGroup

func main(){
	countSize := 5
	waitGroup.Add(countSize)

	for i:=0; i<countSize; i++{
		go func(begin int){
			for j := begin; j < begin+3; j++{
				syncMap.Store(j, j)
			}	
			waitGroup.Done()
		}(i*10)
		
	}

	waitGroup.Wait()
	var size int
	// 统计数量
	syncMap.Range(func(key, value interface{}) bool{
		size++
		return true
	})
	fmt.Println("syncMap current size is " + strconv.Itoa(size))

	// 获取键为 0 的值
	value, ok := syncMap.Load(0)
	if ok {
		fmt.Println("Key 0 has value", value, " ")
	}
}

Pool(临时对象池)

sync.Pool 是 Go 中的一个并发安全的对象池,用于存储和复用临时对象,从而减少内存分配和垃圾回收的压力。

sync.Pool 中的对象是可以被无通知地回收的,而且大小受内存大小限制。

sync.Pool 提供了以下接口:

  • New:当 sync.Pool 中没有可用对象时,调用函数来创建新的对象。
  • Get:从 sync.Pool 中获取一个对象。
  • Put:将对象放回 sync.Pool 中,以便重复使用。
package main

import (
    "fmt"
    "sync"
)

func main() {
	type Student struct{
		id string,
		name string,
		age int
	}
	
    // 初始化 Pool 实例
	var studentPool = sync.Pool{
		New: func() interface{}{
			return new(Student)
		},
	}

	// Get() 从对象池中获取对象
	// 因为返回的是接口,所以通过类型断言提取类型
	stu := studentPool.Get().(*Student)
	stu.name = "Mike"

	// Put() 在对象使用完成后,返回对象池
	studentPool.Put(stu)
}

Atomic(原子操作)

sync/atomic 原子操作是比其它同步技术更基础的操作。原子操作是无锁的,常常直接通过CPU指令直接实现。

对于一个整数类型 Tsync/atomic 标准库包提供了下列原子操作函数。 其中 T 可以是内置 int32int64uint32uint64uintptr 类型。

// 原子地将值加到指定地址
func AddT(addr *T, delta T)(new T)

// 原子地加载值
func LoadT(addr *T) (val T)

// 原子地存储值
func StoreT(addr *T, val T)

// 原子地交换值
func SwapT(addr *T, new T) (old T)

// 原子地比较指定地址值,如果相等就交换
func CompareAndSwapT(addr *T, old, new T) (swapped bool)

sync/atomic 包在 Go 语言中有广泛的应用,特别是在需要高效并发控制的场景下。以下是一些常见的应用场景:

计数器
在高并发环境中,使用 sync/atomic 可以实现线程安全的计数器。例如,统计请求数、任务完成数等。

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var counter int32
    atomic.AddInt32(&counter, 1)
    fmt.Println(counter) // 输出:1
}

标志位
使用原子操作可以实现线程安全的标志位,用于控制程序的执行流程。例如,标记某个任务是否已经完成。

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var flag int32
    atomic.StoreInt32(&flag, 1)
    if atomic.LoadInt32(&flag) == 1 {
        fmt.Println("Flag is set")
    }
}

指针交换
在某些情况下,需要在多个 goroutine 之间安全地交换指针。sync/atomic 提供了原子指针操作,确保指针交换的原子性。

package main

import (
    "fmt"
    "sync/atomic"
    "unsafe"
)

func main() {
    var ptr unsafe.Pointer
    data := 42
    atomic.StorePointer(&ptr, unsafe.Pointer(&data))
    newData := 100
    oldPtr := atomic.SwapPointer(&ptr, unsafe.Pointer(&newData))
    fmt.Println(*(*int)(oldPtr)) // 输出:42
}

共享变量的原子操作
在多线程环境中,直接对共享变量进行非原子操作可能导致数据竞争和竞态条件。使用 sync/atomic 可以确保这些操作的原子性,避免数据竞争。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int32
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            for j := 0; j < 1000; j++ {
                atomic.AddInt32(&counter, 1)
            }
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // 输出:100000
}

4.context

上下文(Context)是 Go 语言中非常有特色的一个特性, 在 Go 1.7 版本中正式引入新标准库 context。其主要的作用是在 goroutine 中进行上下文的传递,而在传递信息中又包含了 goroutine 的运行控制、上下文信息传递等功能。

实际上,Go 语言是基于 context 来实现和搭建各类 goroutine 控制,并和 select 语句联合,可以实现上下文的截止时间、信号控制、信息传递等跨 goroutine 的操作。

context 特性

我们通过调用标准库 context.WithTimeout 方法针对 parentCtx 变量设置了超时时间,并在随后调用 select-case 进行 context.Done 方法的监听,最后由于达到截止时间。因此逻辑上 select 走到了 context.Errcase 分支,最终输出 context deadline exceeded

package main

import (
	"context"
	"fmt"
	"time"
)

func main(){
	// 声明一个空的上下文作为根上下文
	parentCtx := context:Background()

	// 返回一个带有超时时间的子 Context 和 一个取消函数
	ctx, cancel := context.WithTimeout(parentCtx, 1*time.Millisecond)

	// 执行取消函数
	defer cancel()
	
	select {
		// 超时中断
		case <- time.After(1 * time.Second):
			fmt.Println("overslept")
		case <- ctx.Done():
			fmt.Println(ctx.Err())
	}

}

除了上述方法,标准库 context 还支持下面的方法:

// 创建有截止时间的新 context 和 取消函数
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

// 创建一个带有超时时间
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

type Context
	// 创建空的 context,作为根的父级 context
    func Background() Context
	// 创建空的 context,用于未确定时的声明使用
    func TODO() Context
    // 基于某个 context 创建并存储对应的上下文信息
    func WithValue(parent Context, key, val interface{}) Context

context 流程

context 相关函数的返回值分别是 ContextCancelFunc,这里简要分析它们的作用。

Context接口

type Context interface {
	// 获取 context 截止时间
    Deadline() (deadline time.Time, ok bool)
    // 识别只读 channel 是否关闭
    Done() <-chan struct{}
    // 获取被 context 关闭的原因
    Err() error
    // 获取当前 context 所存储的上下文信息
    Value(key interface{}) interface{}
}

Canceler接口

type canceler interface {
	// 调用当前 context 的取消方法
	cancel(removeFromParent bool, err error)
	// 识别当前 channel 是否关闭
	Done() <-chan struct{}
}

在标准库 context 的设计上,提供了四类 context 类型来实现上述接口,分别是 emptyCtxcancelCtxtimerCtx 以及 valueCtx

context 应用场景

context 包在 Go 语言中有许多实际应用,特别是在处理并发任务和网络请求时。以下是几个具体的应用案例:

HTTP 请求处理
在处理 HTTP 请求时,可以使用 context 来管理请求的生命周期,设置超时和传递请求范围内的数据。

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        fmt.Println("Request started")

        select {
        case <-time.After(2 * time.Second):
            fmt.Fprintf(w, "Request completed")
        case <-ctx.Done():
            fmt.Println("Request cancelled:", ctx.Err())
            http.Error(w, ctx.Err().Error(), http.StatusInternalServerError)
        }
    })

    srv := &http.Server{
        Addr: ":8080",
        Handler: http.TimeoutHandler(http.DefaultServeMux, 1*time.Second, "Timeout!"),
    }

    fmt.Println("Server started")
    srv.ListenAndServe()
}

在这个示例中,HTTP 服务器设置了 1 秒的超时时间,如果请求在 1 秒内没有完成,将返回超时错误。

数据库操作
在数据库操作中,可以使用 context 来设置查询的超时和取消操作。

package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
    if err != nil {
        fmt.Println("Error connecting to the database:", err)
        return
    }
    defer db.Close()

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    rows, err := db.QueryContext(ctx, "SELECT * FROM users")
    if err != nil {
        fmt.Println("Query error:", err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        // 处理结果
    }
}

在这个示例中,数据库查询设置了 2 秒的超时时间,如果查询在 2 秒内没有完成,将返回超时错误。

并发任务管理
在并发任务中,可以使用 context 来管理多个 goroutine 的生命周期,确保在任务完成或取消时正确清理资源。

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

func worker(ctx context.Context, wg *sync.WaitGroup, id int) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d cancelled\n", id)
            return
        default:
            fmt.Printf("Worker %d working\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithCancel(context.Background())

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(ctx, &wg, i)
    }

    time.Sleep(2 * time.Second)
    cancel()
    wg.Wait()
    fmt.Println("All workers cancelled")
}

在这个示例中,启动了三个并发的 worker goroutine,并在 2 秒后取消它们。

传递请求范围内的数据
可以使用 context 在函数调用链中传递请求范围内的数据,例如在日志中传递 trace_id

package main

import (
    "context"
    "fmt"
)

func main() {
    ctx := context.WithValue(context.Background(), "trace_id", "123456")
    processRequest(ctx)
}

func processRequest(ctx context.Context) {
    traceID := ctx.Value("trace_id").(string)
    fmt.Println("Trace ID:", traceID)
}

在这个示例中,通过 context.WithValuetrace_id 传递给下游函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值