【go语言】并发编程

news2025/1/30 14:40:51

一、协程、线程、进程

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

import (
	"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 main

import (
	"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

	// 启动多个 goroutine
	for i := 1; i <= 5; i++ {
		wg.Add(1) // 增加一个等待的 goroutine
		go 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 main

import (
	"fmt"
	"sync"
)

var counter int
var mu sync.Mutex // 创建一个互斥锁

func increment() {
	mu.Lock()         // 获取锁
	defer mu.Unlock() // 确保在函数退出时释放锁
	counter++
}

func main() {
	var wg sync.WaitGroup

	// 启动 1000 个 goroutine
	for 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 main

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

var counter int32 // 使用 int32 类型的共享变量

func increment() {
	atomic.AddInt32(&counter, 1) // 对 counter 进行原子加1操作
}

func main() {
	var wg sync.WaitGroup

	// 启动 1000 个 goroutine
	for 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 main

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

var (
	counter int
	mu      sync.RWMutex // 读写锁
)

func read() {
	mu.RLock() // 获取读锁
	defer mu.RUnlock() // 释放读锁
	fmt.Println("Reading counter:", counter)
}

func write(value int) {
	mu.Lock() // 获取写锁
	defer mu.Unlock() // 释放写锁
	counter = value
	fmt.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 main

import (
	"fmt"
)

func main() {
	ch := make(chan string)

	go func() {
		// 发送数据到 channel
		ch <- "Hello, Go!"
	}()

	// 接收数据
	message := <-ch
	fmt.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 main

import (
	"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 main

import (
	"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 监听多个 channel
	select {
	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 main

import (
	"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,提供统一的取消信号,避免不必要的资源消耗。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2284759.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法1-1 模拟与高精度

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

公式与函数的应用

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

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

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

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

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

论文笔记(六十三)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层…

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

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

解决使用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测试

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…

特种作业操作之低压电工考试真题

1.下面&#xff08; &#xff09;属于顺磁性材料。 A. 铜 B. 水 C. 空气 答案&#xff1a;C 2.事故照明一般采用&#xff08; &#xff09;。 A. 日光灯 B. 白炽灯 C. 压汞灯 答案&#xff1a;B 3.人体同时接触带电设备或线路中的两相导体时&#xff0c;电流从一相通过人体流…

[免费]基于Python的Django博客系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的基于Python的Django博客系统&#xff0c;分享下哈。 项目视频演示 【免费】基于Python的Django博客系统 Python毕业设计_哔哩哔哩_bilibili 项目介绍 随着互联网技术的飞速发展&#xff0c;信息的传播与…

进程池的制作(linux进程间通信,匿名管道... ...)

目录 一、进程间通信的理解 1.为什么进程间要通信 2.如何进行通信 二、匿名管道 1.管道的理解 2.匿名管道的使用 3.管道的五种特性 4.管道的四种通信情况 5.管道缓冲区容量 三、进程池 1.进程池的理解 2.进程池的制作 四、源码 1.ProcessPool.hpp 2.Task.hpp 3…

Gurobi 基础语法之 tupledict 和 tuplelist

Python中的字典&#xff1a;dict 我们先来介绍一下Python语法中的 dict 类型, 字典中可以通过任意键值来对数据进行映射&#xff0c;任何无法修改的python对象都可以当作键值来使用&#xff0c;这些无法修改的Python对象包括&#xff1a;整数(比如&#xff1a;1)&#xff0c;浮…

Flutter:搜索页,搜索bar封装

view 使用内置的Chip简化布局 import package:chenyanzhenxuan/common/index.dart; import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import package:get/get.dart; import package:tdesign_flutter/tdesign_flutter.dart;import i…

IoTDB 2025 春节值班与祝福

2025 春节快乐 瑞蛇迎吉庆&#xff0c;祥光映华年&#xff0c;2025 春节已近在眼前。社区祝福 IoTDB 的所有关注者、支持者、使用者 2025 新年快乐&#xff0c;“蛇”来运转&#xff01; IoTDB 团队的春节放假时间为 2025 年 1 月 27 日至 2 月 4 日&#xff0c;1 月 25 日、26…