当前位置: 首页 > article >正文

【go语言】并发编程

一、协程、线程、进程

       在计算机编程中,进程线程协程都是用于并发执行任务的不同概念。他们的区别主要体现在创建、管理和调度的复杂度上,特别是在不同的编程语言中有不同的实现方式。下面是他们的详细区别和在 go 语言中的实现方式。

1.1 进程

  • 定义:进程是程序执行的实例,每个进程都有独立的内存空间和资源操作系统通过进程来管理程序的执行一个进程可以包含多个线程
  • 特点
    • 每个进程有自己的内存空间,进程之间的内存是隔离的
    • 创建和销毁进程的开销较大,因为它需要分配独立的资源(内存、文件句柄等)。
    • 进程之间的通信(IPC)较为复杂,需要使用如管道、共享内存、消息队列等机制。

1.2 线程

  • 定义线程是进程中的一个执行单元,多个线程共享同一进程的内存和资源线程的调度由操作系统进行管理
  • 特点
    • 线程之间共享进程的内存空间,但每个线程有自己的栈空间
    • 创建和销毁线程比进程更高效,因为不需要为每个线程分配独立的资源。
    • 线程之间的通信相对较为容易,但也需要小心避免共享数据时的同步问题(例如死锁、竞争条件等)。

1.3 协程

  • 定义:协程是 Go 语言中的轻量级线程实现,它是由 Go 运行时(runtime)管理的,并且通常比操作系统线程更加高效。协程是用户级别的线程,创建和调度开销非常小
  • 特点
    • 协程由 Go 运行时调度,运行时负责管理协程的生命周期、调度等,通常会比操作系统线程的上下文切换更加高效。
    • 协程使用的是栈空间,通常非常小,初始栈大小为 2KB,动态增长。这使得可以在单个程序中启动成千上万的协程。
    • 协程之间可以通过 Go 语言的通道(channel)进行通信,具有内建的并发支持。
    • 由于协程由 Go 运行时调度,可以使用更少的系统资源,因此它们比操作系统线程更加高效。

1.4 go 语言中的并发:协程和通道

       在 Go 语言中,协程和通道是处理并发的核心。Go 语言通过关键字 go 启动一个新的协程,运行时会负责协程的调度。协程之间的通信通常通过 channel 来完成,channel 可以确保协程间的数据安全传输。

package mainimport ("fmt""time"
)func sayHello(ch chan string) {ch <- "Hello, World!"
}func main() {ch := make(chan string)  // 创建一个通道go sayHello(ch)  // 启动一个协程// 从通道中接收数据并打印fmt.Println(<-ch)// 为了防止主程序过早退出,可以加个延时time.Sleep(time.Second)
}
  • go sayHello(ch) 启动了一个新的协程执行 sayHello 函数。
  • ch 是一个通道,用于在协程之间传递数据。
  • fmt.Println(<-ch) 从通道中接收数据并打印。
  • 进程:最基本的执行单元,内存和资源是隔离的。
  • 线程:进程中的执行单元,线程之间共享内存,但每个线程有独立的栈。
  • 协程:Go 语言中的轻量级线程,由 Go 运行时管理,创建和调度效率高,内存开销小,支持高并发。

1.5 最简单的 goroutine

func main() {go func() {fmt.Println("mmm")}// 如果这里不加时间进行等待,会导致程序直接停止
}

二、go 语言的 gmp 调度原理

       go 语言的 gmp 调度模型是 go 语言的并发执行模型的核心,他提供了一种高效的方式来管理大量的 goroutine(轻量级线程)。gmp 是 go 语言运行时调度器的基础,代表 GoroutineMachineProcessor 三个核心组件。

2.1 解释一下 GMP 的意思 

2.1.1 G (Goroutine)

  • Goroutine 是 Go 语言中的轻量级线程。每个 goroutine 在 Go 中是由程序员创建的,可以认为是一个协作式的线程。在 Go 中,通过 go 关键字启动一个 goroutine。Go runtime 会管理这些 goroutine 的调度与执行。
  • Goroutine 是非常轻量的,相比于操作系统的线程,它们的开销更小。Go runtime 会动态地分配 goroutine 到可用的 P 上。

2.1.2 M (Machine)

  • Machine 对应操作系统的线程。一个 M 代表着一个真正的操作系统线程,它会与操作系统调度程序一起工作,执行实际的工作负载。每个 M 都运行在一个操作系统线程上,可以有多个 M 运行在多核 CPU 上。
  • 一个 M 通常负责调度和执行一个或多个 goroutine

2.1.3 P (Processor)

  • Processor 代表调度器中的一个“逻辑处理器”,是 Go runtime 管理调度的核心单元。P 管理着一组可用的 M 和 goroutine。它会决定哪个 M 可以运行哪个 goroutine。
  • 一个 P 管理着一组待执行的 goroutine 队列,也就是运行时的可执行 goroutine(称为 run queue)。P 会将 goroutine 分配到 M 上执行。

2.2 GMP 的工作原理

       GMP 模型的主要思想是通过调度器来实现 goroutine 的高效调度。他通过将多个 goroutine 分配给多个操作系统来并行处理任务。调度器会根据机器的 CPU 核心数、Goroutine 数量以及每一个 M 和 P 的工作负载来灵活地分配任务。

2.3 GMP 详细工作流程

  1. Goroutine 创建与调度:

    • 当你使用 go 关键字创建一个 goroutine 时,Go runtime 会将这个 goroutine(G)添加到调度队列中。然后,调度器会通过选择适当的 P 来运行这个 goroutine。
  2. G、P 和 M 的绑定与分配:

    • 每个 M 会有一个固定的 P,这样它就能执行和调度 goroutine。一个 P 只能绑定一个 M 来执行其管理的 goroutine,但是一个 M 可以有多个 P(通过时间片轮转)。
    • P 上的 goroutine 会被分配给空闲的 M 执行。当某个 M 执行完其 goroutine 后,它会向调度器请求新的 goroutine 来执行。如果 P 上有未执行完的 goroutine,M 就会从 P 上的队列中选择并执行。
  3. P、M 和 G 的协作:

    • 运行时通过每个 P 管理多个 goroutine。当一个 P 上的 goroutine 被执行时,M 会将其从 P 的队列中取出并执行。每当 M 执行完当前 goroutine,它会查看 P 上是否有待执行的任务。如果没有,M 就会尝试向其他 P 借取 goroutine 来执行。
    • 如果某个 M 执行的任务需要进行 IO 操作或阻塞,它会主动将自己挂起,释放对 CPU 的占用,以便其他 M 可以继续执行。
  4. 负载均衡与调度:

    • Go runtime 会通过负载均衡机制来确保系统的 CPU 核心资源得到有效利用。如果某个 P 的 goroutine 队列为空,而其他 P 的队列中有待执行的任务,Go runtime 会将任务从其他 P 中迁移到当前 P,保证资源的高效利用。
    • 每个 M 在执行时都在一个执行队列中轮流调度执行 goroutine,如果一个 M 完成了自己的任务,它可能会被“偷”走工作去执行其他任务。
  5. 工作窃取:

    • 为了避免 CPU 空闲,Go 的调度器会使用“工作窃取”机制。当某个 P 没有任务可执行时,它会向其他 P 申请任务。即,如果 P 上的任务队列为空,P 可以从其他 P 上“窃取”未完成的任务,从而实现任务负载的均衡。
  6. 协作式调度与抢占式调度:

    • Go 的调度器主要是协作式调度,即 goroutine 在执行时会主动让出 CPU 让其他 goroutine 执行。这意味着当 goroutine 执行完一个函数时,它可能会主动挂起,让其他 goroutine 执行。
    • 然而,Go runtime 也在一些情况下会执行抢占式调度,例如当 goroutine 执行时间过长,或者阻塞某个 P 时,系统会强制切换到其他 goroutine。

2.4 G、P、M 如何协作

假设系统有 2 个 CPU 核心,启动 10 个 goroutine。

  1. 启动时,Go runtime 会启动 2 个 M,并为它们分配 2 个 P。每个 P 管理一部分 goroutine。
  2. 每个 P 会分配到若干个 goroutine,当一个 P 里的 goroutine 被执行完时,它会向其他 P 请求任务,或者从其它 P 中“窃取”任务。
  3. 如果某个 M 上的 goroutine 由于阻塞(如 IO 操作),它会被挂起,Go runtime 会安排其他 M 去执行其他 goroutine。

       Go 语言的 GMP 模型通过将 goroutine 与操作系统线程(M)和逻辑处理器(P)之间的协调与调度,最大限度地提高了并发执行效率。它有效地解决了轻量级并发的管理和调度问题,并且能够高效地利用多核 CPU 资源。通过工作窃取和负载均衡机制,Go 能够在大规模并发的情况下,保持系统的高效运行和稳定性。

三、WaitGroup 的使用

       sync.WaitGroup 是 go 语言中用于等待一组 goroutine 完成的同步源语。他常用于并发编程中,特别是在多个 goroutine 启动后,主程序或者其他 goroutine 需要等待他们全部完成才能继续执行的场景。

sync.WaitGroup 主要提供了以下方法:

  1. Add (int):增加计数器的值,可以是正数或负数。通常用来增加正在等待的 goroutine 数量。
  2. Done():调用时会将计数器减 1,表示某个 goroutine 已经完成。
  3. Wait():阻塞当前 goroutine,直到计数器的值变为 0,表示所有的 goroutine 都已完成。
package mainimport ("fmt""sync""time"
)func worker(id int, wg *sync.WaitGroup) {defer wg.Done() // 标记当前 goroutine 完成fmt.Printf("Worker %d starting\n", id)time.Sleep(time.Second) // 模拟工作fmt.Printf("Worker %d done\n", id)
}func main() {var wg sync.WaitGroup// 启动多个 goroutinefor i := 1; i <= 5; i++ {wg.Add(1) // 增加一个等待的 goroutinego worker(i, &wg)}// 等待所有的 goroutine 完成wg.Wait()fmt.Println("All workers are done")
}
  1. 创建 sync.WaitGroup:在 main 函数中,首先声明了一个 sync.WaitGroup 类型的变量 wg

  2. wg.Add(1):在启动每个 goroutine 之前,调用 wg.Add(1),表示我们等待一个新的 goroutine 完成。1 是递增的数量,表示需要等待一个 goroutine。

  3. defer wg.Done():在 worker 函数中,每个 goroutine 在完成时都会调用 wg.Done(),这会将 WaitGroup 的计数器减 1,表示该 goroutine 已经完成。

  4. wg.Wait():在 main 函数中,调用 wg.Wait() 会阻塞,直到所有的 goroutine 调用 Done(),使计数器变为 0,main 函数才能继续执行。

 注意:

  • Add() 的调用时机:通常应该在启动 goroutine 之前调用 Add(),确保计数器正确地反映等待的 goroutine 数量。如果在 goroutine 启动后再调用 Add(),有可能会导致程序死锁,因为在 Wait() 等待时计数器已经为 0。

  • 避免并发修改 WaitGroupWaitGroupAdd()Done() 方法是并发安全的,但不能在多个 goroutine 同时调用 Add(),否则可能会引发竞态条件。一般可以在启动 goroutine 之前集中调用 Add()

四、互斥锁和原子变量

4.1 互斥锁

       在 go 语言中,互斥锁是一个常用的同步原语,用于保护共享资源在并发环境中的访问。他确保同一时刻只有一个 gorooutine 能够访问共享资源,从而避免数据竞争和不一致的状态。

4.1.1 互斥锁的作用

  • 锁定共享资源:在多 goroutine 并发访问同一资源时,使用互斥锁来确保只有一个 goroutine 可以访问共享资源,其他的 goroutine 必须等待锁释放后才能访问。
  • 防止数据竞争:通过在对共享数据进行操作时加锁,可以防止不同的 goroutine 同时修改该数据,避免出现不一致的情况。

4.1.2 互斥锁的使用方法

Go 语言的 sync 包提供了 Mutex 类型,它有两个主要的方法:

  1. Lock():尝试获取锁。如果锁已被其他 goroutine 持有,当前 goroutine 会阻塞,直到锁被释放。
  2. Unlock():释放锁,使其他 goroutine 可以获得锁。
package mainimport ("fmt""sync"
)var counter int
var mu sync.Mutex // 创建一个互斥锁func increment() {mu.Lock()         // 获取锁defer mu.Unlock() // 确保在函数退出时释放锁counter++
}func main() {var wg sync.WaitGroup// 启动 1000 个 goroutinefor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait() // 等待所有 goroutine 完成fmt.Println("Counter:", counter) // 输出最终计数
}
  1. 创建 Mutex:在代码中,声明了一个 sync.Mutex 类型的变量 mu,它将用于控制对共享变量 counter 的访问。

  2. Lock()Unlock():每次对共享变量 counter 进行修改时,我们会调用 mu.Lock() 获取锁,确保同一时刻只有一个 goroutine 可以修改 counter。函数结束时(使用 defer)会调用 mu.Unlock() 释放锁,允许其他 goroutine 获取锁。

  3. 并发增加计数:启动了 1000 个 goroutine,每个 goroutine 执行 increment 函数,增加 counter 的值。由于互斥锁的保护,虽然有多个 goroutine 同时在运行,但它们会按照顺序访问 counter,避免了数据竞争。

  4. wg.Wait():我们使用 sync.WaitGroup 等待所有 goroutine 完成。

4.1.3 锁的最佳实践

  1. 避免死锁:死锁发生在两个或多个 goroutine 相互等待对方释放锁的情况下。为了避免死锁,应该确保获取锁的顺序一致。

    • 比如,如果有多个锁需要获取,确保所有 goroutine 按照相同的顺序去锁定这些资源。
  2. 尽量缩小临界区:在锁住的区域中,避免做大量计算或者 I/O 操作。锁住的时间越长,越容易导致性能问题和其他 goroutine 的阻塞。

  3. 尽量避免过多的锁竞争:当多个 goroutine 在同一时刻争用一个锁时,会导致性能下降。在设计时尽量减少锁的粒度,可以使用其他同步原语(如 sync.RWMutex 或通道)来优化性能。

4.2 原子变量

       在 Go 语言中,原子操作(Atomic operations)提供了一种在不使用传统锁(如 sync.Mutex)的情况下,安全地对共享变量进行并发访问的方法。这种方式通常用于避免锁带来的性能开销,同时确保数据的一致性和原子性。

       Go 提供了 sync/atomic 包来进行原子操作,支持对基本数据类型(如 int32int64uint32uint64uintptr 等)进行原子读写操作。

4.2.1 原子操作的特性

原子操作具有以下特性:

  1. 不可分割性:原子操作要么完全执行,要么完全不执行,不会被其他线程中断。
  2. 线程安全:多个 goroutine 同时操作同一变量时,原子操作保证操作是安全的,不会发生数据竞争。

4.2.2 常见的原子操作

sync/atomic 包提供了几个常用的原子操作函数,包括:

  • AddInt32AddInt64:对整数进行原子加法操作。
  • LoadInt32LoadInt64:读取整数的原子操作。
  • StoreInt32StoreInt64:写入整数的原子操作。
  • CompareAndSwapInt32CompareAndSwapInt64:执行原子比较和交换操作,常用于实现无锁算法。 
package mainimport ("fmt""sync""sync/atomic"
)var counter int32 // 使用 int32 类型的共享变量func increment() {atomic.AddInt32(&counter, 1) // 对 counter 进行原子加1操作
}func main() {var wg sync.WaitGroup// 启动 1000 个 goroutinefor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait() // 等待所有 goroutine 完成fmt.Println("Counter:", counter) // 输出最终计数
}

4.2.3 常见的原子操作函数

1. atomic.AddInt32 和 atomic.AddInt64 

原子地将指定的值加到整数变量上。

atomic.AddInt32(&x, 1) // x = x + 1
atomic.AddInt64(&x, 2) // x = x + 2
2. atomic.LoadInt32 和 atomic.LoadInt64

原子地读取整数变量的值。

val := atomic.LoadInt32(&x) // 获取 x 的值
3. atomic.StoreInt32 和 atomic.StoreInt64

原子地将一个值存储到整数变量。

atomic.StoreInt32(&x, 42) // 将 42 存储到 x 中
4. atomic.CompareAndSwapInt32 和 atomic.CompareAndSwapInt64

原子地进行比较并交换操作。它会检查变量的值是否等于指定值,如果是,才会将其更新为新值。这通常用于实现锁的自旋等无锁算法。

success := atomic.CompareAndSwapInt32(&x, old, new) // 如果 x == old,x = new,返回 true,否则返回 false

4.2.4 使用原子操作的注意事项

  • 只适用于基本数据类型sync/atomic 包只支持对一些基本类型(如 int32int64uintptr 等)进行原子操作,不能直接对复合类型(如数组、切片、结构体等)进行原子操作。
  • 操作必须是无符号的或 32 位/64 位整数:只有符合这些条件的数据类型才能使用原子操作。
  • 有竞争时的性能问题:尽管原子操作避免了使用互斥锁,但如果并发量过大,多个 goroutine 频繁竞争同一个原子变量,可能会导致性能下降。因此,还是需要合理设计并发模型。

4.2.5 适合使用原子操作的场景

原子操作通常适用于以下场景:

  1. 计数器:例如统计请求次数、执行次数等。
  2. 标志位:用来表示某些状态,比如“是否已经完成”。
  3. 无锁队列/栈:使用原子操作实现更高效的并发数据结构。
  4. 基于CAS(比较并交换)的无锁算法:许多无锁数据结构(如队列、栈等)是通过 CompareAndSwap 实现的。

4.3 读写锁

       在 Go 语言中,读写锁sync.RWMutex)提供了比普通互斥锁(sync.Mutex)更灵活的锁机制,适用于读多写少的场景。与普通互斥锁不同,读写锁允许多个 goroutine 并发地读取共享资源,只在写操作时才会互斥。读锁和写锁的限制不一样,读锁只互斥写锁,而写锁需要互斥读锁和写锁

4.3.1 读写锁的工作原理

  • 读锁(RLock):多个读锁可以并发持有,允许多个 goroutine 同时读取数据。
  • 写锁(Lock):写锁是独占的,意味着只有一个 goroutine 可以持有写锁,同时其他任何 goroutine(无论是读还是写)都不能访问受保护的数据。
  • 读写锁通过分离读锁和写锁,优化了读操作多于写操作的场景,减少了锁竞争。

4.3.2 使用 sync.RWMutex

sync.RWMutex 是 Go 语言提供的读写锁,包含以下方法:

  • RLock():请求读锁,如果有其他写锁或读锁被持有,它会阻塞当前 goroutine,直到读锁可以获得。
  • RUnlock():释放读锁。
  • Lock():请求写锁,写锁是独占的,它会阻塞其他所有的读锁和写锁请求。
  • Unlock():释放写锁。
package mainimport ("fmt""sync""time"
)var (counter intmu      sync.RWMutex // 读写锁
)func read() {mu.RLock() // 获取读锁defer mu.RUnlock() // 释放读锁fmt.Println("Reading counter:", counter)
}func write(value int) {mu.Lock() // 获取写锁defer mu.Unlock() // 释放写锁counter = valuefmt.Println("Writing counter:", counter)
}func main() {var wg sync.WaitGroup// 模拟多个 goroutine 读取共享资源for i := 0; i < 5; i++ {wg.Add(1)go func(i int) {defer wg.Done()read()}(i)}// 模拟写操作wg.Add(1)go func() {defer wg.Done()write(42)}()// 等待所有 goroutine 完成wg.Wait()
}

4.3.3 使用场景

读写锁在以下场景下非常有用:

  • 读多写少:如果你的程序大部分时间都是读取数据,且写操作相对较少,使用读写锁可以大大提高并发性能。
  • 频繁查询:比如缓存读取等,多个查询操作可以并发执行,不需要等待其他读取操作完成。
  • 数据一致性要求较高:在进行写操作时,确保其他操作(无论读写)都无法同时进行,避免并发修改导致数据不一致。
  • 缓存系统:比如缓存的读取是频繁的,而更新缓存的操作则较少。
  • 共享配置:多 goroutine 读取配置,偶尔更新配置的场景。
  • 多线程数据查询:多 goroutine 并发查询共享数据,且查询操作多于更新操作时。

4.3.4 读写锁的优缺点

优点

  1. 提高并发性:对于读操作非常频繁的场景,多个 goroutine 可以并发读取,增加了并发度。
  2. 减少竞争:写操作相对较少时,多个读操作可以并行,减少了锁竞争,提高性能。

缺点

  1. 写操作会阻塞所有读操作和其他写操作:如果有写锁,所有的读锁和其他写锁都会被阻塞,可能导致写操作成为瓶颈。
  2. 复杂性:与普通互斥锁相比,读写锁更复杂,可能会增加死锁的风险(例如,如果不正确释放锁,或者在获取读锁后尝试获取写锁)。

五、通道 channel

       在 Go 语言中,channel 是一种用于 goroutine 之间通信的机制,它可以让一个 goroutine 将数据传递给另一个 goroutine,从而实现数据同步和协作。channel 是 Go 的并发编程模型的核心部分之一。

5.1 基本概念

  • 发送:通过 channel 发送数据,另一个 goroutine 可以从该 channel 中接收数据。
  • 接收:接收来自 channel 的数据,通常用于同步和数据传递。
  • 无缓冲与有缓冲:channel 可以是无缓冲的(即发送方和接收方必须同步进行)或有缓冲的(即有一定容量,发送方不必等待接收方)。

5.2 创建和使用 channel

1. 创建 Channel

通过 make 函数创建一个 channel:

  • 无缓冲 channel

ch := make(chan int)
  •  有缓冲 channel(指定容量):
ch := make(chan int, 3) // 创建一个容量为 3 的缓冲 channel
2. 发送和接收数据
  • 发送数据:使用 <- 操作符将数据发送到 channel:

ch <- 42 // 将 42 发送到 channel
  • 接收数据:使用 <- 操作符从 channel 中接收数据:

value := <-ch // 从 channel 接收数据,并将其赋值给 value
3. 关闭 Channel

       一个 channel 在不再需要时应该关闭,这样可以通知接收方没有更多的数据发送过来。关闭 channel 使用 close 函数:

close(ch) // 关闭 channel

       关闭后,接收方会接收到一个零值,并且可以通过检查 channel 是否关闭来判断是否还需要继续接收数据。 

5.3 无缓冲 channel 实例

package mainimport ("fmt"
)func main() {ch := make(chan string)go func() {// 发送数据到 channelch <- "Hello, Go!"}()// 接收数据message := <-chfmt.Println(message)
}

go 语言中的 happen-before 机制

       在 Go 语言中,happen-before 机制是并发编程中的一个重要概念,用于描述事件的发生顺序关系。在多线程或多 goroutine 环境下,确保某些操作的顺序是至关重要的,特别是在共享数据时。Go 的并发模型(基于 goroutine 和 channel)通过一些规则来确保操作的顺序关系,避免数据竞争和不一致的状态。

什么是 happen-before 机制?

       happen-before 是一个用于描述程序中操作之间因果顺序的规则。在并发编程中,happen-before 机制定义了如何确保一个操作在另一个操作之前发生,并且保证一个操作的结果能够对其他操作可见。

具体而言,happen-before 机制可以通过以下几种方式来实现:

  1. 程序顺序规则(Program Order Rule):在同一个 goroutine 内,代码的执行顺序是保证的,即一个操作发生在前一个操作之后。

  2. 同步规则(Synchronization Rule):一个 goroutine 对 channel 的发送操作(ch <- x)happen-before 在同一个 channel 上的接收操作(x := <-ch)。即,发送方操作先发生,接收方能够看到发送方的结果。

  3. 锁顺序规则(Lock Rule):如果一个 goroutine 锁定了某个对象(例如通过 sync.Mutexsync.RWMutex)并在解锁之前执行了某些操作,这些操作对持锁 goroutine 内部的其他操作是可见的。解锁操作的发生,保证了锁定对象的修改对其他尝试获取同一锁的 goroutine 可见。

  4. 发布-订阅规则(Publish-Subscribe Rule):如果一个 goroutine 写入共享变量(例如通过 channel 或共享内存),并且另一个 goroutine 通过某种同步机制(如 channel)读取该变量,则写入操作对读取操作是可见的。即,写入操作 "发布" 了数据,读取操作 "订阅" 了数据。

5.4 有缓冲 channel 实例

package mainimport ("fmt"
)func main() {ch := make(chan string, 2) // 创建一个缓冲区大小为 2 的 channel// 启动多个 goroutine 发送数据go func() {ch <- "Hello"ch <- "Go"close(ch) // 发送完数据后关闭 channel}()// 接收数据for msg := range ch {fmt.Println(msg)}
}

5.5 使用 select 语句

       Go 语言中的 select 语句类似于 switch,但它用于多个 channel 操作,能够在多个 channel 中选择一个可操作的 channel 执行。

package mainimport ("fmt""time"
)func main() {ch1 := make(chan string)ch2 := make(chan string)go func() {time.Sleep(2 * time.Second)ch1 <- "From channel 1"}()go func() {time.Sleep(1 * time.Second)ch2 <- "From channel 2"}()// 使用 select 监听多个 channelselect {case msg1 := <-ch1:fmt.Println(msg1)case msg2 := <-ch2:fmt.Println(msg2)}
}

六、context 包

       go 语言的 context 包提供了在并发编程中管理上下文信息的机制,主要用于处理比如取消信号、超时控制、截止时间和请求范围的值传递等问题。context 包的设计目的是为了在多个 goroutine 中传递和管理操作的声明周期并提供在这些 goroutine 中进行取消或者超时控制的能力

6.1 主要功能

  1. 取消操作(Cancellation):可以通过上下文传递取消信号,通知所有与之相关的 goroutine 停止执行。
  2. 超时控制(Timeout):可以设置一个超时限制,超过时间后自动取消操作。
  3. 截止时间(Deadline):可以指定一个具体的时间点,超过该时间点后自动取消操作。
  4. 传递值(Values):可以在上下文中存储值,便于在 goroutine 中共享状态或其他信息。

6.2 常见类型和函数

1. context.Context 接口
  • context.Context 是 context 包的核心类型,它是一个接口,定义了操作上下文的方法。其他的上下文类型都实现了这个接口。常用方法如下:
  • Done() <-chan struct{}:返回一个通道,当上下文被取消或超时时,会关闭该通道。
  • Err() error:如果上下文已经被取消或超过了截止时间,返回一个相应的错误(如 context.Canceled 或 context.DeadlineExceeded)。
  • Value(key interface{}) interface{}:返回上下文中与 key 关联的值。
2. context.Background()
  • 返回一个空的上下文,通常用于根上下文。它是最顶层的上下文,通常作为其他上下文的父上下文。
3. context.TODO()
  • 用于不确定使用哪个上下文的情况,通常在未确定是否需要上下文或尚未实现相关逻辑时使用。
4. context.WithCancel(parent Context)
  • 返回一个新的上下文和一个取消函数。如果调用了返回的取消函数,则新上下文的 Done() 通道会关闭。
  • ctx, cancel := context.WithCancel(context.Background())
    cancel() // 会关闭 ctx.Done() 通道
    
5. context.WithTimeout(parent Context, timeout time.Duration)
  • 返回一个新的上下文,该上下文会在指定的时间后自动取消。如果时间到达,Done() 通道会关闭。
  • ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel() // 确保上下文被取消
    
6. context.WithDeadline(parent Context, deadline time.Time)
  • 返回一个新的上下文,该上下文会在指定的时间点自动取消。
  • deadline := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()
    
7. context.WithValue(parent Context, key, val interface{})
  • 返回一个新的上下文,它携带了一个键值对。这个方法常用于在请求的上下文中传递请求范围内的数据(如数据库连接、用户身份信息等)。
  • ctx := context.WithValue(context.Background(), "userID", 12345)
    userID := ctx.Value("userID")
    fmt.Println(userID) // 输出: 12345
    

package mainimport ("context""fmt""time"
)func doWork(ctx context.Context) {select {case <-time.After(3 * time.Second): // 模拟长时间工作fmt.Println("Work completed")case <-ctx.Done(): // 超时或被取消fmt.Println("Work canceled or timed out:", ctx.Err())}
}func main() {// 创建一个 2 秒超时的上下文ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel() // 确保取消// 启动 goroutine 执行任务go doWork(ctx)// 等待任务完成time.Sleep(4 * time.Second) // 等待超过超时的时间
}
Work canceled or timed out: context deadline exceeded

6.3 使用场景

  1. 处理 HTTP 请求:在 Web 服务中,通常会将 context 与 HTTP 请求关联,用于管理请求的生命周期,控制请求超时、取消等。
  2. 数据库操作:在进行数据库操作时,使用 context 来控制查询的超时,确保在超时后不再继续执行操作。
  3. 并发任务管理:在并发编程中,使用 context 来协调多个 goroutine,提供统一的取消信号,避免不必要的资源消耗。

相关文章:

【go语言】并发编程

一、协程、线程、进程 在计算机编程中&#xff0c;进程、线程和协程都是用于并发执行任务的不同概念。他们的区别主要体现在创建、管理和调度的复杂度上&#xff0c;特别是在不同的编程语言中有不同的实现方式。下面是他们的详细区别和在 go 语言中的实现方式。 1.1 进程 定义…...

算法1-1 模拟与高精度

目录 一 阶乘数码 二 麦森数 三 模拟题 一 阶乘数码 本题中n<1000,1000的阶乘为以下这么大&#xff0c;远超long的范围 402387260077093773543702433923003985719374864210714632543799910429938512398629020592044208486969404800479988610197196058631666872994808558901…...

JS中对数组的操作哪些会改变原数组哪些不会?今天你一定要记下!

JavaScript 数组方法&#xff1a;变更原数组与不变更原数组的区别 在 JavaScript 中&#xff0c;数组是非常常见且重要的数据结构。作为开发者&#xff0c;我们常常需要使用数组方法来处理数组数据。但是&#xff0c;数组的不同方法会以不同的方式影响原数组&#xff0c;它们可…...

公式与函数的应用

一 相邻表格相乘 1 也可以复制 打印标题...

ShenNiusModularity项目源码学习(7:数据库结构)

ShenNiusModularity项目默认使用mysql数据库&#xff0c;数据库连接字符串放到了ShenNius.Admin. Mvc、ShenNius.Admin.Hosting的appsettings.json文件内。   ShenNiusModularity项目为自媒体内容管理系统&#xff0c;支持常规管理、CMS管理、商城管理等功能&#xff0c;其数…...

【STL笔记】字符串

字符串 下标从0开始&#xff0c;常规用法不再赘述&#xff0c;持续更新中… 1. substr(pos&#xff0c;len): 返回从位置 pos 开始&#xff0c;长度为 len 的子串。(len默认为npos) std::string str "Hello, World!"; std::string sub1 str.substr(7, 5); // 提…...

java知识点 | java中不同数据结构的长度计算

在Java中&#xff0c;size 和 length是两个不同的属性&#xff0c;分别用于不同的数据结构。以下是它们的详细区别和适用场景&#xff1a; 1.length 适用对象&#xff1a; 数组&#xff08;Array&#xff09;&#xff1a;数组是一个固定长度的线性数据结构&#xff0c;其长度是…...

WordPress event-monster插件存在信息泄露漏洞(CVE-2024-11396)

免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...

手撕Diffusion系列 - 第九期 - 改进为Stable Diffusion(原理介绍)

手撕Diffusion系列 - 第九期 - 改进为Stable Diffusion&#xff08;原理介绍&#xff09; 目录 手撕Diffusion系列 - 第九期 - 改进为Stable Diffusion&#xff08;原理介绍&#xff09;DDPM 原理图Stable Diffusion 原理Stable Diffusion的原理解释Stable Diffusion 和 Diffus…...

AI软件栈:LLVM分析(一)

文章目录 AI 软件栈后端编译LLVM IRLLVM的相关子项目AI 软件栈后端编译 AI软件栈的后端工作通常与硬件架构直接相关,为了实现一个既能适配现代编程语言、硬件架构发展的目标,所以提出了LLVM具备多阶段优化能力提供基础后端描述,便于进行编译器开发兼容标准编译器的行为LLVM …...

编程语言中的常见Bug及解决方案

在编程过程中&#xff0c;不同语言有其独特的特性和挑战&#xff0c;这也导致了各种常见Bug的出现。本文将总结几种主流编程语言中的常见Bug&#xff0c;包括JavaScript、Python、C/C、Java和Go&#xff0c;并提供相应的解决方案和案例。 一、JavaScript中小数相加精度不准确的…...

论文笔记(六十三)Understanding Diffusion Models: A Unified Perspective(三)

Understanding Diffusion Models: A Unified Perspective&#xff08;三&#xff09; 文章概括 文章概括 引用&#xff1a; article{luo2022understanding,title{Understanding diffusion models: A unified perspective},author{Luo, Calvin},journal{arXiv preprint arXiv:…...

修改maven的编码格式为utf-8

1.maven默认编码为GBK 注:配好MAVEN_HOME的环境变量后,在运行cmd. 打开cmd 运行mvn -v命令即可. 2.修改UTF-8为默认编码. 设置环境变量 变量名 MAVEN_OPTS 变量值 -Xms256m -Xmx512m -Dfile.encodingUTF-8 3.保存,退出cmd.重新打开cmd 运行mvn -v命令即可. 源码获取&…...

从AD的原理图自动提取引脚网络的小工具

这里跟大家分享一个我自己写的小软件&#xff0c;实现从AD的原理图里自动找出网络名称和引脚的对应。存成文本方便后续做表格或是使用简单行列编辑生成引脚约束文件&#xff08;如.XDC .UCF .TCL等&#xff09;。 我们在FPGA设计中需要引脚锁定文件&#xff0c;就是指示TOP层…...

Coze,Dify,FastGPT,对比

在当今 AI 技术迅速发展的背景下&#xff0c;AI Agent 智能体成为了关键领域&#xff0c;Coze、Dify 和 FastGPT 作为其中的佼佼者&#xff0c;各有千秋。 平台介绍 - FastGPT&#xff1a;由环界云计算公司发起&#xff0c;是基于大语言模型&#xff08;LLM&#xff09;的开源…...

【数据结构】_链表经典算法OJ(力扣版)

目录 1. 移除链表元素 1.1 题目描述及链接 1.2 解题思路 1.3 程序 2. 反转链表 2.1 题目描述及链接 2.2 解题思路 2.3 程序 3. 链表的中间结点 3.1 题目描述及链接 3.2 解题思路 3.3 程序 1. 移除链表元素 1.1 题目描述及链接 原题链接&#xff1a;203. 移除链表…...

【数据结构】(1)集合类的认识

一、什么是数据结构 1、数据结构的定义 数据结构就是存储、组织数据的方式&#xff0c;即相互之间存在一种或多种关系的数据元素的集合。 2、学习数据结构的目的 在实际开发中&#xff0c;我们需要使用大量的数据。为了高效地管理这些数据&#xff0c;实现增删改查等操作&…...

Vue 3 中的 TypeScript:接口、自定义类型与泛型

在 Vue 3 中&#xff0c;TypeScript 提供了强大的类型系统&#xff0c;帮助我们更好地管理代码的类型安全。通过使用 接口&#xff08;Interface&#xff09;、自定义类型&#xff08;Type Aliases&#xff09; 和 泛型&#xff08;Generics&#xff09;&#xff0c;我们可以编…...

计算机组成原理(计算机系统3)--实验七:新增指令实验

一、实验目标 了解RISC-V mini处理器架构&#xff0c;在其基础之上新增一个指令&#xff0c;完成设计并观察指令执⾏。 二、实验内容 1) 修改数据通路&#xff0c;新增指令comb rs1,rs2,rd采用R型指令格式&#xff0c;实现将rs1高16位和rs2低16位拼接成32位整数&#xff0c;…...

LeetCode 0040.组合总和 II:回溯 + 剪枝

【LetMeFly】40.组合总和 II&#xff1a;回溯 剪枝 力扣题目链接&#xff1a;https://leetcode.cn/problems/combination-sum-ii/ 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates…...

解决使用Selenium时ChromeDriver版本不匹配问题

在学习Python爬虫过程中如果使用Selenium的时候遇到报错如下session not created: This version of ChromeDriver only supports Chrome version 99… 这说明当前你的chrome驱动版本和浏览器版本不匹配。 例如 SessionNotCreatedException: Message: session not created: This…...

CAN波特率匹配

STM32 LinuxIMX6ull&#xff08;Linux&#xff09;基于can-utils测试...

JVM垃圾回收器的原理和调优详解!

全文目录&#xff1a; 开篇语前言摘要概述垃圾回收器分类及原理1. Serial 垃圾回收器2. Parallel 垃圾回收器3. CMS 垃圾回收器4. G1 垃圾回收器 源码解析示例代码 使用案例分享案例 1&#xff1a;Web 服务的 GC 调优案例 2&#xff1a;大数据任务的 GC 优化 应用场景案例垃圾回…...

与机器学习相关的概率论重要概念的介绍和说明

概率论一些重要概念的介绍和说明 1、 试验 &#xff08;1&#xff09;试验是指在特定条件下&#xff0c;对某种方法、技术、设备或产品&#xff08;即&#xff0c;事物&#xff09;进行测试或验证的过程。 &#xff08;2&#xff09;易混淆的概念是&#xff0c;实验。实验&…...

JavaScript中的相等运算符:`==`与`===`

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…...

A7. Jenkins Pipeline自动化构建过程,可灵活配置多项目、多模块服务实战

服务容器化构建的环境配置构建前需要解决什么下面我们带着问题分析构建的过程:1. 如何解决jenkins执行环境与shell脚本执行环境不一致问题?2. 构建之前动态修改项目的环境变量3. 在通过容器打包时避免不了会产生比较多的不可用的镜像资源,这些资源要是不及时删除掉时会导致服…...

66-《虞美人》

虞美人 虞美人&#xff08;学名&#xff1a;Papaver rhoeas L.&#xff09;&#xff1a;一年生草本植物&#xff0c;全体被伸展的刚毛&#xff0c;稀无毛。茎直立&#xff0c;高25-90厘米&#xff0c;具分枝。叶片轮廓披针形或狭卵形&#xff0c;羽状分裂&#xff0c;裂片披针形…...

obsidian插件——Metadata Hider

原本是要找导出图片时显示属性的插件&#xff0c;奈何还没找到&#xff0c;反而找到了可以隐藏属性的插件。唉&#xff0c;人生不如意&#xff0c;十之八九。 说一下功能&#xff1a; 这个插件可以把obsidian的文档属性放在右侧显示&#xff0c;或者决定只显示具体几项属性&a…...

MySQL中InnoDB逻辑存储结构

在MySQL中&#xff0c;InnoDB是最常用的存储引擎之一&#xff0c;它具有高度的事务支持、行级锁、ACID特性以及自动崩溃恢复等特性。InnoDB的逻辑存储结构可以分为多个层次&#xff0c;下面是详细的解析。 1. 表空间 (Tablespace) InnoDB的物理存储结构以表空间为基础。表空间…...

高阶C语言|深入理解字符串函数和内存函数

文章目录 前言1.求字符串长度1.1 字符串长度函数&#xff1a;strlen模拟实现 长度不受限制的字符串函数1.2 字符串拷贝函数&#xff1a;strcpy模拟实现 1.3 字符串连接函数&#xff1a;strcat模拟实现 1.4 字符串比较函数&#xff1a;strcmp模拟实现 长度受限制的字符串函数2.1…...