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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

day6手机摄影社区,可以去苹果摄影社区学习拍摄技巧

逛自己手机的社区&#xff1a;即&#xff08;手机牌子&#xff09;摄影社区 拍照时防止抖动可以控制自己的呼吸&#xff0c;不要大喘气 拍一张照片后&#xff0c;如何简单的用手机修图&#xff1f; HDR模式就是让高光部分和阴影部分更协调&#xff08;拍风紧时可以打开&…

1905电影网中国地区电影数据分析(一) - 数据采集、清洗与存储

文章目录 前言一、数据采集步骤及python库使用版本1. python库使用版本2. 数据采集步骤 二、数据采集网页分析1. 分析采集的字段和URL1.1 分析要爬取的数据字段1.2 分析每部电影的URL1.2 分析每页的URL 2. 字段元素标签定位 三、数据采集代码实现1. 爬取1905电影网分类信息2. 爬…

Qpython+Flask监控添加发送语音中文信息功能

对QpythonFlask实现对小孩学习的监控-CSDN博客中html页面进行改造&#xff0c;利用Ajax&#xff0c;提交一段文字&#xff0c;发送到数据库&#xff0c;再在服务器&#xff0c;发送该段文件给手机端&#xff0c;然手机端TTS朗读出来&#xff0c;增加了父母监控小孩学习&#xf…

【note】MCTS

MCTS survey 参考 http://arxiv.org/abs/2103.04931 基本概念 MDP 可以表示为一个四元组 ( S , A S , P a , P w ) (S,A_S,P_a,P_w) (S,AS​,Pa​,Pw​)&#xff1a; S S S&#xff1a;状态空间 A s A_s As​&#xff1a;状态 s s s 下的可行动作集合 P a ( s , s ′ ) P_…

Couchbase UI: Server

在 Couchbase UI 中的 Server&#xff08;服务器&#xff09;标签页主要用于管理和监控集群中的各个节点。以下是 Server 标签页的主要内容和功能介绍&#xff1a; 1. 节点列表 显示集群中所有节点的列表&#xff0c;每个节点的详细信息包括&#xff1a; 节点地址&#xff1…

顶刊JFR|ROLO-SLAM:首个针对不平坦路面的车载Lidar SLAM系统

摘要 基于激光雷达&#xff08;LiDAR&#xff09;的同步定位与地图构建&#xff08;SLAM&#xff09;被认为是在恶劣环境中提供定位指导的一种有效方法。然而&#xff0c;现成的基于激光雷达的SLAM方法在经过不平坦地形时&#xff0c;尤其是在垂直方向相关的部分&#xff0c;会…

枪支消音器的 CFD 模拟

探索应用于枪支消音器的计算流体动力学的迷人世界。 了解枪支消音器 枪支消音器&#xff0c;也称为抑制器&#xff0c;是安装在枪支枪管上的装置&#xff0c;用于降低子弹发射时产生的噪音。消音器的作用是减缓和冷却子弹离开枪管时迅速膨胀的热气体。这一过程有助于降低声音…

Object类(1)

大家好&#xff0c;今天我们来学习一个常用类-Object类&#xff0c;这个类可以说是所有类的父类&#xff0c;因此它的重要性不言而喻&#xff0c;那么话不多说&#xff0c;来看。 大家可能会有一个疑问&#xff0c;java中不是只能继承一个类吗&#xff0c;那么我们继承了Objec…

【数据资产】数据资产管理概述

导读&#xff1a;数据资产管理在企业的数字化转型和业务发展中扮演着至关重要的角色。它直接关系到企业的决策效率、运营优化、业务创新以及风险防控等多个方面。数据资产作为企业的重要战略资源&#xff0c;能够为企业带来经济利益&#xff0c;其价值可能来自于数据本身的稀缺…

Qt监控系统辅屏预览/可以同时打开4个屏幕预览/支持5x64通道预览/onvif和rtsp接入/性能好

一、前言说明 在监控系统中&#xff0c;一般主界面肯定带了多个通道比如16/64通道的画面预览&#xff0c;随着电脑性能的增强和多屏幕的发展&#xff0c;再加上现在监控摄像头数量的增加&#xff0c;越来越多的用户希望在不同的屏幕预览不同的实时画面&#xff0c;一个办法是打…

51单片机开发:独立键盘实验

实验目的&#xff1a;按下键盘1时&#xff0c;点亮LED灯1。 键盘原理图如下图所示&#xff0c;可见&#xff0c;由于接GND&#xff0c;当键盘按下时&#xff0c;P3相应的端口为低电平。 键盘按下时会出现抖动&#xff0c;时间通常为5-10ms&#xff0c;代码中通过延时函数delay…

【翻转硬币——莫比乌斯函数、分块、卷积、埃氏筛】

题目 暴力代码&#xff0c;官网过55% #include <bits/stdc.h> using namespace std; int main() {int n;cin >> n;vector<bool> a(n 1);a[1] 1;int res 1;for (int i 2; i < n; i){if (a[i] 0){for (int j i; j < n; j i)a[j] a[j] ^ 1;res;}…

Hive:内部表和外部表,内外转换

内部表和外部表 内部表示例 给表添加数据 外部表示例 给表添加数据 外部表示例 用location指定表目录位置,那么表的位置在实际指定的位置,但是可以被映射 外部表和内部表的区别 删除表后使用show tables in shao; 已经没有被删除的表,说明元数据已经被删除(mysql里面存放),但是…

算法题(49):反转链表II

审题&#xff1a; 需要我们对指定范围的链表进行反转&#xff0c;并返回反转后链表的头结点 思路&#xff1a; 方法一&#xff1a;vector法 我们先遍历一次链表&#xff0c;并把数据对应的存在数组中&#xff0c;然后利用数组的reverse方法进行反转数据&#xff0c;最后再遍历一…

Unreal Engine 5 C++ Advanced Action RPG 十一章笔记

第十一章 In Game Widgets 本章节就是做UI2-Template Button Widget 这章节创建不同的UI 结束UI胜利UI暂停菜单主菜单加载UI新建一个按钮小组件作为模版 3-Pause Menu Template Button 继续做更多模版UI 4-Lose Screen(游戏失败UI) 做失败的UI 之前按钮模版的调度程序就在这起…

基于OpenCV实现的答题卡自动判卷系统

一、图像预处理 🌄 二、查找答题卡轮廓 📏 三、透视变换 🔄 四、判卷与评分 🎯 五、主函数 六、完整代码+测试图像集 总结 🌟 在这篇博客中,我将分享如何使用Python结合OpenCV库开发一个答题卡自动判卷系统。这个系统能够自动从扫描的答题卡中提取信…

Go:基于Go实现一个压测工具

文章目录 写在前面整体架构通用数据处理模块Http请求响应数据处理Curl参数解析处理 客户端模块Http客户端处理Grpc客户端处理Websocket客户端处理 连接处理模块GrpcHttp 统计数据模块统计原理实现过程 写在前面 本篇主要是基于Go来实现一个压测的工具&#xff0c;关于压测的内…

ES6 简单练习笔记--变量申明

一、ES5 变量定义 1.在全局作用域中 this 其实就是window对象 <script>console.log(window this) </script>输出结果: true 2.在全局作用域中用var定义一个变量其实就相当于在window上定义了一个属性 例如: var name "孙悟空" 其实就相当于执行了 win…

2025数学建模美赛|赛题翻译|E题

2025数学建模美赛&#xff0c;E题赛题翻译 更多美赛内容持续更新中...

Android AOP:aspectjx

加入引用 在整个项目的 build.gradle 中&#xff0c;添加 classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10" 可以看到测试demo的 gradle 版本是很低的。 基于 github 上的文档&#xff0c;可以看到原版只支持到 gradle 4.4 。后续需要使…