5.1 并发概述
5.1.1进程与线程
- 程序是磁盘上编译好的二进制文件,不占用系统资源(CPU、内存、设备)。进程是活跃的程序,占用系统资源,在内存中执行。程序运行起来,产生一个进程。程序就像是剧本,进程就像是演员演戏,一个是静态的,一个是动态的。
- 线程也叫轻量级进程,是真正运行在cpu上的执行单元,通常一个进程包含多个线程。
5.1.2 协程
- 协程可以理解为用户态线程,是更微量级别的线程;却别与线程,协程的调度在用户态进行,不需要切换到内核态,所以不由os参与,由用户自己控制。
- 用户态与内核态差异:https://zhuanlan.zhihu.com/p/388057431 低权限的资源范围较小,高权限的资源范围更大,所谓的「用户态与内核态只是不同权限的资源范围」
- 一个进程上可以跑多个线程,一个线程上可以跑多个协程
- 协程要比线程的切换速度快,因为线程在时间片切换的时候要保存上下文,协程间切换只需要保存任务的上下文,没有内核的开销。
5.1.3 并行与并发
- 并行是把一个任务分配给每一个处理器独立完成,在同一时间点,多个任务同时进行
5.2 Goroutine(Go语言中的协程)
- go语言的并发只会用到协程(goroutine),并不需要我们去考虑多进程或者多线程。
- 在 Go 语言中,各个 goroutine 是平等的,它们的执行顺序由调度器决定。
- 在某个任务需要并发执行的时候,只需要把这个任务包转成一个函数,开启一个goroutine去执行这个函数就可以了。并不需要我们来维护一个类似线程池的东西,也不需要我们去关心协程是怎么切换和调度的,因为这些都已经有go语言内置的调度器帮我们做了。
- 介绍:https://cloud.tencent.com/developer/article/2227925
5.3 Channel
- 介绍:https://zhuanlan.zhihu.com/p/613771870
通道(channel)是 Go 语言中用于在多个 Go 协程之间进行通信的一种方式。通道具有以下特性:
- 阻塞:当向通道发送数据时,如果通道已满,发送操作会阻塞直到通道有空间可用;当从通道接收数据时,如果通道为空,接收操作会阻塞直到通道中有值可接收。
- 同步:通道可以被用于在不同的 Go 协程之间进行同步。比如,一个协程可以向通道发送数据,另一个协程可以从通道接收数据,这样就可以实现协程之间的同步操作。
- 无缓冲和有缓冲:通道可以是无缓冲的,也可以是有缓冲的。无缓冲通道保证发送和接收操作是同步的,发送和接收的协程都会阻塞直到另一端准备好。有缓冲通道可以在一定程度上解耦发送和接收操作,只有当缓冲区满时发送操作才会阻塞,只有当缓冲区空时接收操作才会阻塞。
- 单向通道:可以将通道限制为只读或只写,这样可以在一定程度上提高程序的类型安全性。
通道是 Go 语言并发编程中非常重要的一个概念,它提供了一种简单而有效的方式来进行协程间的通信和同步。 |
- 管道用完后需要对其进行关闭,避免程序一直在等待以及资源的浪费。但是关闭的管道,仍然可以从中接收数据,只是接收的数据永远是各数据类型的默认值。
- channel智能杯关闭一次,对同一个channel重复关闭会产生panic
- 从一个已经关闭的channel读取消息不会发生panic,会一直读取所有的数据,直到零值
- channel可以读端和写端都可有多个goroutine操作,在一段关闭channel的时候,该channel读端的所有goroutine都会收到channel已关闭的消息
- channel是并发安全的,多个goroutine同时读取channel中的数据,不会产生并发安全问题
5.4 Sync
- 介绍:https://developer.aliyun.com/article/1425301
以下是 sync 包中一些常用的类型和函数:
sync.Mutex: 互斥锁,用于在代码块中创建一个临界区,以确保在任意时刻只有一个 goroutine 可以访问临界区代码。
Go goCopy code var mutex sync.Mutex mutex.Lock() // 临界区代码 mutex.Unlock() |
sync.RWMutex: 读写互斥锁,允许多个 goroutine 同时读取共享数据,但在写入时需要独占锁。
- 同时只能有一个goroutine能够获得写锁定
- 同时可以有任意多个goroutine获得读锁定
- 同时只能存在写锁定或读锁定(读和写互斥)
- 通俗理解就是可以多个goroutine同时读,但是只有一个goroutine能写,共享资源要么在被一个或多个goroutine读取,要么再被一个goroutine写入,读写不能同时进行。
Go goCopy code var rwMutex sync.RWMutex rwMutex.RLock() // 读取共享数据// 读取操作 rwMutex.RUnlock() rwMutex.Lock() // 写入共享数据// 写入操作 rwMutex.Unlock() |
sync.WaitGroup: 用于等待一组 goroutine 完成其任务。
Go goCopy code var wg sync.WaitGroup wg.Add(1) go func() {defer wg.Done()// 执行任务 }() wg.Wait() // 等待所有任务完成 |
- sync.WaitGroup对象的计数器不能为负数,否则会panic,在使用的过程中,我们需要保证Add()的参数值,以及执行完Done()之后计数器大于等于0.
sync.Cond: 条件变量,用于在多个 goroutine 之间传递信号。
Go goCopy code var cond = sync.NewCond(&sync.Mutex{}) cond.L.Lock() for !condition() { cond.Wait() // 等待条件满足 } // 条件满足后执行操作 cond.L.Unlock() |
sync.Once: 用于执行一次性操作,确保函数在并发情况下只执行一次。
- 在我们写项目的时候,程序中有很多的逻辑只需要执行一次,最典型的就是项目工程里配置文件的加载,我们只需要加载一次即可,让配置保存在内存中,下次使用的时候直接使用内存中的配置数据即可。这里就要用到sync.Once
- sync.Once可以在代码的任意位置初始化和调用,并且线程安全。sync.Once最大的作用就是延迟初始化,对于一个sync.Once变量我们并不会在程序启动的时候初始化,而是在第一次用到它的时候才会初始化,并且只初始化这一次,初始化后就驻留在内存里,这就非常适合我们之前提到的配置文件加载场景,设想一下,如果是在程序刚开始就加载配置,若迟迟未被使用,则既浪费了内存,又延长了程序加载时间。
Go goCopy code var once sync.Once once.Do(func() {// 执行一次性操作 }) |
- 与init() 的区别:有时候我们使用init()方法进行初始化,init()方法是在其所在的package搜词加载时执行的,二sync.Once可以在代码的任意位置初始化和调用,是在第一次用它的时候才会初始化。
死锁
如果将带有锁结构的变量赋值给其他变量,锁的状态会复制。所以多锁复制后的新的锁拥有原来的锁状态。若外层的main函数已经Lock一次了,但是没有机会Unlock,会导致将外层锁传入到其他函数的锁拥有lock状态,导致内层函数一直等待Lock,而外层函数一直等待Unlock,这样就造成了死锁。
所以在使用锁的时候,我们应当避免锁拷贝,并且保证Lock()和Unlock()成对出现,没有成对出现容易出现死锁的情况,或者是Unlock一个为加锁的Mutex而导致panic。
sync.Map
sync.Map 是 Go 语言中提供的一种并发安全的映射(Map)类型。与内建的 map 类型不同的是,sync.Map 不需要在并发访问时加锁,因为它内部已经实现了并发安全。
sync.Map 的主要优点是它提供了一种高效的并发安全的键值对存储和检索的机制,而且无需显式地使用互斥锁来保护数据结构。这使得它在某些场景下可以更高效地处理并发访问。
下面是一个简单的示例,展示了如何使用 sync.Map:
Go goCopy code package main
import ("fmt""sync" )
func main() {// 创建一个 sync.Mapvar m sync.Map // 存储键值对 m.Store("key1", "value1") m.Store("key2", "value2") // 获取值 val1, ok1 := m.Load("key1") fmt.Println("Value of key1:", val1, "Found:", ok1) // 输出: Value of key1: value1 Found: true// 删除键值对 m.Delete("key2") // 获取不存在的键值对 val2, ok2 := m.Load("key2") fmt.Println("Value of key2:", val2, "Found:", ok2) // 输出: Value of key2: <nil> Found: false// 遍历键值对 m.Range(func(key, value interface{}) bool { fmt.Println("Key:", key, "Value:", value)return true }) } |
sync.Map 的方法有:
- Load(key interface{}) (value interface{}, ok bool): 获取键对应的值。
- Store(key, value interface{}): 存储键值对。
- Delete(key interface{}): 删除键值对。
- Range(f func(key, value interface{}) bool): 遍历所有键值对,f 是一个回调函数,当返回 false 时停止遍历。
需要注意的是,sync.Map 在性能上可能会比普通的 map 类型稍慢,所以在一般情况下,如果不需要并发安全,可以优先选择普通的 map 类型。但在一些需要并发安全的场景下,特别是并发访问频繁的情况下,sync.Map 可以提供更好的性能和安全性。
atomic
- 使用方式:通常mutex用于保护一段执行逻辑,而atomic主要是对变量进行操作
Go func LoadInt32(addr *int32) (val int32) 从指定的内存地址addr加载一个int32类型的值,并返回该值。 func StoreInt32(addr *int32, val int32) 将int32类型的值val存储到指定的内存地址addr。 func AddInt32(addr *int32, delta int32) (new int32) 将int32类型的值delta加到指定的内存地址addr所指向的值上,并返回新的值。 func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) 如果指定的内存地址addr所指向的值等于old,则将其设置为new,并返回true;否则不做任何操作,返回false。 func SwapInt32(addr *int32, new int32) (old int32) 将指定的内存地址addr所指向的值设置为new,并返回原来的值。 // 除了上述针对int32类型的方法外,sync/atomic包还提供了针对int64、uintptr、unsafe.Pointer等类型的原子操作方法, 如LoadInt64、StoreUintptr、CompareAndSwapPointer等。 这些方法都能够在并发环境中安全地进行原子性操作,避免数据竞争的问题。 |
Go package main
import ( "fmt" "sync" "sync/atomic" )
func main() { var sum int32 = 0 var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() atomic.AddInt32(&sum, 1) }()
} wg.Wait() fmt.Printf("sum is %d\n", sum) } |
atomic.Value
假设想对多个变量进行同步保护,即假设想对一个struct这样的符合类型使用院子操作,就可以用atomic.Value。
atomic.Value提供了四种方法:
- func (v *Value) Load() (x interface{})
- 加载当前存储的值,并返回该值。在并发场景下,加载操作是原子的。
- func (v *Value) Store(x interface{})
- 将一个值存储到atomic.Value中。存储操作是原子的。
- func (v *Value) Swap(new interface{}) (old interface{})
- 原子性地将新值new存储到atomic.Value中,并返回旧值。
- CompareAndSwap: func(v *Value)CompareAndSwap(old ,new any)(swapped bool) 通常用于原子性地比较当前存储的值和预期值,如果相等,则替换为新值。这种操作在一些原子操作中是非常常见的
Go package main
import ( "fmt" "sync/atomic" )
type Student struct { Name string Age int }
func main() { st1 := Student{ Name: "zhangsan", Age: 18, } st2 := Student{ Name: "lisi", Age: 19, } st3 := Student{ Name: "wangwu", Age: 20, }
var v atomic.Value v.Store(st1) fmt.Println("what have v stored:", v.Load().(Student))
old := v.Swap(st2) fmt.Printf("after swap: v=v%v\n", v.Load().(Student)) fmt.Printf("after swap: old=%v\n", old)
swapped := v.CompareAndSwap(st1, st3) fmt.Printf("compare %v and %v\n", swapped, v) swapped01 := v.CompareAndSwap(st2, st3) fmt.Printf("compare %v and %v\n", swapped01, v) } /** what have v stored: {zhangsan 18} after swap: v=v{lisi 19} after swap: old={zhangsan 18} compare false and {{lisi 19}} compare true and {{wangwu 20}} **/ |
sync.pool
sync.Pool 是 Go 语言中提供的一个对象池,用于缓存和复用临时对象,以减少内存分配和垃圾回收的压力。sync.Pool 的特点是它是并发安全的,可以在多个 goroutine 之间共享,适用于并发环境下的对象复用场景。
sync.Pool 的主要作用是减少内存分配和垃圾回收的开销,特别是在一些需要频繁创建和销毁临时对象的场景下,比如连接池、缓冲池等。通过对象池,可以避免频繁地进行内存分配和释放,从而提高程序的性能。
sync.Pool 的操作包括:
- New:sync.Pool的构造函数,用于指定sync.Pool中缓存的数据类型,当调用Get方法从对象池中获取对象的时候,对象吃中如果没有,会调用New方法创建一个新的对象。
- Get: 从池中获取对象,如果池中没有对象,则返回 nil。
下面是一个简单的示例,演示了如何使用 sync.Pool:
Go package main
import ( "fmt" "sync" )
type Student struct { Name string Age int }
func main() { pool := sync.Pool{ New: func() interface{} { return &Student{ "ISAAC", 22, } }, } st := pool.Get().(*Student) println(st.Name, st.Age) fmt.Printf("addr is %p\n", st)
pool.Put(st) st1 := pool.Get().(*Student) println(st1.Name, st1.Age) fmt.Printf("addr is %p\n", st1) } |
在上面的Go代码中,我们创建了一个sync.Pool类型的对象pool,用于实现对象的重用。sync.Pool是Go语言提供的一个对象池,可以用来存储临时对象并在需要时重复利用,避免频繁地创建和销毁对象,从而提高程序的性能。 在sync.Pool中,我们使用Get()方法从对象池中获取对象,并使用类型断言将其转换为*Student类型。这里为什么要进行类型断言呢?主要有以下几个原因:
- 接口类型的返回值:sync.Pool的Get()方法返回的是interface{}类型的值,即空接口类型。空接口类型可以存储任意类型的值,但在使用时需要将其转换为具体的类型,这就需要使用类型断言来实现。
- 类型安全:通过类型断言,我们可以在编译期间就确定被转换的对象的具体类型,避免在运行时出现类型错误导致程序崩溃的情况。
- 代码可读性:在代码中使用类型断言,可以使代码更加清晰明了,让其他开发者更容易理解代码的含义和逻辑。
在这段代码中,我们通过类型断言将从对象池中取出的对象转换为*Student类型,并进一步操作该对象。这样可以确保我们在使用对象时能够按照其具体类型进行处理,保证程序的正确性和稳定性。 |
sync .pool的使用场景
- sync.pool主要是通过对象复用来降低gc带来的性能损耗,所以在高并发场景下,由于每个goroutine都可能过于频繁地创建一些大对象,造成gc压力很大。所以在高并发业务场景下出现GC问题时,可以使用sync.pool减少gc负担
- sync.pool不适合存储带状态的对象,比如socket链接、数据库连接等,因为里面的对象随时可能会被gc回收释放掉
- 不适合需要控制缓存对象个数的场景,因为Pool池里面的对象个数时随机变化的,因为池子里的对象会被gc的,且释放时机是随机的。
5.5 Select
select是什么?
select是go语言提供的一种多路复用机制,用于检测当前goroutine连接的多个channel是否有数据准备完成。 GO语言的select语句,是用来起一个goroutine监听多个channel的读写事件,提高从多个channel获取信息的效率,相当于也是单线程处理多个IO事件。 注意事项:
- 没有default且case无法执行的select永久阻塞
- 当多个case都准备好了的时候,会随机选择一个执行。
- 在 Go 语言中,当 select 语句中的某个 case 被执行后,select 语句会立即退出,而不会继续向下执行其他 case。
|
select语句的语法如下:
Go select { case <-ch1:// 执行操作1 case <-ch2:// 执行操作2 default:// 默认操作(可选) } |
select语句的每个case必须是一个通信操作,要么是发送数据到channel,要么是从channel接收数据。select会阻塞,直到其中一个case可以运行,然后执行相应的操作。
Go package main
import ( "fmt" "time" )
func main() { ch1 := make(chan string) ch2 := make(chan string)
go func() { time.Sleep(4 * time.Second) ch1 <- "Hello" }()
go func() { time.Sleep(3 * time.Second) ch2 <- "World" }()
select { case msg1 := <-ch1: fmt.Println("Received message from ch1:", msg1) case msg2 := <-ch2: fmt.Println("Received message from ch2:", msg2) case <-time.After(3 * time.Second): fmt.Println("Timeout") } } |
没太搞懂default的触发?
select 在执行过程中,必须命中其中的某一分支。 如果在遍历完所有的 case 后,若没有命中(命中:也许这样描述不太准确,我本意是想说可以执行信道的操作语句)任何一个 case 表达式,就会进入 default 里的代码分支。
Go package main
import ( "fmt" "time" )
func main() { ch1 := make(chan string) ch2 := make(chan string)
go func() { time.Sleep(4 * time.Second) ch1 <- "Hello" }() go func() { //time.Sleep(3 * time.Second) ch2 <- "World" }() //这句是关键,由于主线程执行优先于与其他线程,若是没有这一句, // 则没有时间片给到其他线程,导致channel中没有数据 time.Sleep(time.Second)
select { case msg1 := <-ch1: fmt.Println("Received message from ch1:", msg1) case msg2 := <-ch2: fmt.Println("Received message from ch2:", msg2) case <-time.After(3 * time.Second): fmt.Println("Timeout") default: fmt.Println("default!") } } | |
5.6 Context
Context接口结构:
Go type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}} |
Context接口包含四个方法:
- Deadline返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false。
- Done 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil。
- Err 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded。
- Value 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil。
|
介绍:https://www.zhihu.com/tardis/zm/art/110085652?source_id=1005
用途:
1. 用于并发控制
2.上下文的信息传递
总的来说,Context就是用来在父子goroutine间进行值传递以及发送cancel信号的一种机制。
context.WithCancel
Go package main
import ( "context" "fmt" "time" )
func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit\n", name) return default: fmt.Printf("%s watching...\n", name) time.Sleep(time.Second) } } }
func main() { ctx, cancel := context.WithCancel(context.Background()) go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2")
time.Sleep(6 * time.Second) fmt.Println("end watching!") cancel() time.Sleep(time.Second) } /* goroutine2 watching... goroutine1 watching... goroutine1 watching... goroutine2 watching... goroutine2 watching... goroutine1 watching... goroutine2 watching... goroutine1 watching... goroutine2 watching... goroutine1 watching... goroutine2 watching... goroutine1 watching... end watching! goroutine2 exit goroutine1 exit */ |
在这段代码中,首先定义了一个Watch函数,该函数接收一个context.Context类型的上下文和一个名称参数。在Watch函数中,使用select语句监听上下文的Done通道,一旦上下文被取消(ctx.Done()可被关闭),就会打印出相应的信息并返回,结束Watch函数的执行。
在main函数中,首先创建了一个带有取消函数的上下文ctx,然后启动了两个goroutine分别调用Watch函数,并传入不同的名称。随后程序会等待6秒钟,然后输出"end watching!",再调用cancel函数取消上下文,从而触发goroutines中的Watch函数执行ctx.Done()分支,输出相应信息并结束循环。
contetx.WithDeadline
Go package main
import ( "context" "fmt" "time" )
func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second)) defer cancel() go Watch01(ctx, "goroutine1") go Watch01(ctx, "goroutine2")
time.Sleep(6 * time.Second) fmt.Println("end watching!") }
func Watch01(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit\n", name) return default: fmt.Printf("%s watching...\n", name) time.Sleep(time.Second) } } } |
在这段代码中,main函数创建了一个带有截止时间的上下文ctx,截止时间为当前时间加上4秒,同时使用defer cancel()延迟取消该上下文。然后启动了两个goroutine分别调用Watch01函数,并传入不同的名称。
Watch01函数与之前的Watch函数逻辑类似,不同之处在于现在使用的是带有截止时间的上下文。在Watch01函数中,同样监听上下文的Done通道,一旦超过截止时间或者上下文被取消,就会打印出相应的信息并返回,结束Watch01函数的执行。
在main函数中,程序会等待6秒钟,然后输出"end watching!",此时可能会看到部分输出为"exit",表示某些goroutine在截止时间内被取消,另一些可能因为截止时间未到而继续打印"watching..."信息,直至最后全部结束。
这段代码展示了如何使用带有截止时间的上下文来控制goroutine的执行,确保在给定时间内完成任务或进行超时处理。
context.WithTImeout
Go package main
import ( "context" "fmt" "time" )
func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() go Watch01(ctx, "goroutine1") go Watch01(ctx, "goroutine2")
time.Sleep(6 * time.Second) fmt.Println("end watching!") }
func Watch01(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit\n", name) return default: fmt.Printf("%s watching...\n", name) time.Sleep(time.Second) } } } |
这段代码与之前的代码非常相似。主要的区别在于使用了context.WithTimeout()来创建带有超时时间的上下文。
在main函数中,通过context.WithTimeout(context.Background(), 4*time.Second)创建了一个上下文ctx,超时时间为4秒,并使用defer cancel()延迟取消该上下文。然后启动了两个goroutine分别调用Watch01函数,并传入不同的名称。
Watch01函数的逻辑与之前相同,使用select语句监听上下文的Done通道,一旦超时时间到达或者上下文被取消,就会打印出相应的信息并返回,结束Watch01函数的执行。
在main函数中,程序会等待6秒钟,然后输出"end watching!"。在这段时间内,根据超时时间的设置,某些goroutine可能会在4秒内被取消,而另一些可能会继续打印"watching..."信息,直至最后全部结束。
通过使用带有超时时间的上下文,可以在一定时间内控制goroutine的执行,避免长时间等待或处理超时情况。
context.WithValue
context.WithValue(parent Context, key interface{}, val interface{}) Context 函数接收三个参数:
- parent: 父 Context,表示要在其基础上创建新的 Context。
- key: 用于标识键值对数据的键,通常是一个任意类型的值,可用于唯一标识这个键值对数据。
- val: 要存储的键值对数据的值,可以是任意类型的数据。
Go package main
import ( "context" "fmt" "time" )
func func1(ctx context.Context) { fmt.Printf("name is: %s\n", ctx.Value("name").(string)) }
func main() { ctx := context.WithValue(context.Background(), "name", "zhangsan") go func1(ctx) time.Sleep(time.Second) } |
在上面的示例中,首先创建了一个父 Context,然后使用 context.WithValue 函数在父 Context 的基础上增加了一个键值对数据。最后通过 ctx.Value("key") 方法从新的 Context 中获取键值对数据,并打印出来。
使用 context.WithValue 可以方便地在 Context 中传递一些额外的信息,但需要注意的是,Context 是不可变的,每次调用 context.WithValue 都会返回一个新的 Context 对象,而不会修改原有的 Context 对象。
5.7 定时器
项目中常常会有这样的场景,比如到了未来某一时刻,需要某个逻辑或者某个任务执行一次,或者是周期性地执行多次,有点类似定时任务。这种场景就需要用到定时器,golang中也内置了定时器的实现,timer和ticker。
Timer
time.Timer 的结构定义如下:
Go type Timer struct { C <-chan Time// 包含隐藏或嵌入字段 } |
time.Timer 类型实际上是一个带有 C 字段的结构体,其中 C 是一个只能接收 Time 类型的通道。在 Go 语言中,使用 <-timer.C 可以从定时器的通道中接收时间事件。
除了这个公开的字段外,time.Timer 类型可能还包含一些隐藏或嵌入的字段,用于支持定时器的内部实现。
Go package main
import ( "fmt" "time" )
func main() { timer := time.NewTimer(2 * time.Second) <-timer.C fmt.Println("after 2s Time out!") } |
这段代码的原理是基于 time.Timer 的机制实现的。让我们来解释一下它的原理:
- 调用 time.NewTimer(2 * time.Second) 创建了一个新的定时器,该定时器将在 2 秒后触发。
- <-timer.C 这行代码会从 timer.C 通道中接收值。在定时器到期之前,这个接收操作会阻塞程序的执行。一旦定时器到期,timer.C 通道会发送一个时间事件值,这时 <-timer.C 就会接收到这个值,然后程序继续执行。
这种阻塞的机制使得程序可以等待定时器到期而不需要进行显式的轮询操作。当定时器到期时,阻塞状态解除,程序可以继续执行相应的逻辑。 |
阻塞
阻塞会暂时打断程序的正常执行。在 Go 语言中,阻塞通常指的是某个 goroutine 在等待某个操作完成时被挂起,无法继续向下执行。具体来说:
- 发送操作的阻塞:当一个 goroutine 尝试向一个已满的通道发送数据时,这个 goroutine 将被阻塞,直到有其他 goroutine 从该通道中接收数据为止。
- 接收操作的阻塞:当一个 goroutine 尝试从一个空的通道接收数据时,这个 goroutine 将被阻塞,直到有其他 goroutine 向该通道发送数据为止。
- 定时器的阻塞:在使用定时器时,比如调用 <-timer.C 进行等待定时器触发的操作时,当前的 goroutine 将会被阻塞,直到定时器触发或被停止。
在以上情况下,被阻塞的是发起发送或接收操作的 goroutine。被阻塞的 goroutine 会暂时挂起,直到条件满足后才能继续执行。
Stop()与Reset()
Go package main
import ( "fmt" "time" )
func main() { timer := time.NewTimer(2 * time.Second) <-timer.C res := timer.Stop() fmt.Println("after 2s Time out!") fmt.Printf("res is %v\n", res)
timer.Reset(3 * time.Second)
res1 := timer.Stop() fmt.Printf("res1 is %v\n", res1) } |
- Stop 方法:
- Stop 方法用于停止定时器,如果定时器尚未触发,则返回 true;如果定时器已经触发或者已经被停止过,则返回 false。
- 在代码中,当定时器到期时,会打印 "after 2s Time out!",然后调用 timer.Stop() 来停止定时器。由于定时器已经触发,所以 res 的值为 false。
- Reset 方法:
- Reset 方法用于重置定时器,并将定时器的超时时间设置为指定的时间间隔。如果定时器已经触发,调用 Reset 方法会重新开始计时。
- 在代码中,调用 timer.Reset(3 * time.Second) 将定时器的超时时间设置为 3 秒。在此之后,又调用了 timer.Stop() 来停止定时器。由于定时器还未触发,所以 res1 的值为 true。
总结一下:
- Stop 方法用于停止定时器,返回值表示是否成功停止。
- Reset 方法用于重置定时器的超时时间,并重新开始计时。
|
Afterfunc
Go package main
import ( "fmt" "time" )
func main() { duration := time.Duration(1) * time.Second
f := func() { fmt.Println("f has been called after 1s by time.Afterfunc") } timer := time.AfterFunc(duration, f)
defer timer.Stop()
time.Sleep(3 * time.Second) } |
ime.AfterFunc 函数的定义如下:
Go func AfterFunc(d Duration, f func()) *Timer |
time.AfterFunc 函数会在指定的时间间隔 d 之后,调用函数 f。它会返回一个 time.Timer 对象,通过该对象可以取消定时器。 在你的代码中,time.AfterFunc(duration, f) 会在 1 秒之后调用函数 f。而在 main 函数中,使用 defer timer.Stop() 来确保在 main 函数执行结束时停止定时器。然后通过 time.Sleep(3 * time.Second) 来等待足够的时间,以便触发定时器的回调函数。 需要注意的是,defer 语句会在 main 函数执行结束时才被执行,因此 timer.Stop() 会在主函数执行结束时调用,而不会在定时器触发之后立即执行。 |
After
Go package main
import ( "fmt" "time" )
func main() { ch := make(chan string)
go func() { time.Sleep(time.Second * 3) ch <- "test" }() select { case val := <-ch: fmt.Printf("val is %s\n", val) case <-time.After(time.Second * 2): fmt.Println("timeout!") } } |
这段代码中使用了 select 语句和 time.After 函数来处理 channel 接收超时的情况。让我解释一下代码的执行流程:
- 在 main 函数中,首先创建了一个字符串类型的 channel ch。
- 然后启动了一个 goroutine,在该 goroutine 中会经过 3 秒后向 channel ch 中发送字符串 "test"。
- 接着是 select 语句,它会同时监听两个 case:
- 第一个 case 是从 channel ch 中接收数据,如果在 3 秒内成功接收到数据,就会打印出接收到的值。
- 第二个 case 使用 time.After 函数,在 2 秒后会向一个内置的 channel 发送时间信息,进而触发该 case,这里会打印出 "timeout!"。
- 由于第一个 case 中的数据发送操作需要 3 秒才会完成,而第二个 case 在 2 秒后就会触发超时,所以在这种情况下,第二个 case 会被执行,输出 "timeout!"。
因此,当运行这段代码时,由于接收数据的操作需要 3 秒,而超时设置为 2 秒,所以最终会输出 "timeout!"。 |
Ticker
Go package main
import ( "fmt" "time" )
func Watch() chan struct{} { ticker := time.NewTicker(1 * time.Second)
ch := make(chan struct{})
go func(ticker *time.Ticker) { defer ticker.Stop() for { select { case <-ticker.C: fmt.Println("Watching!") case <-ch: fmt.Println("Ticker stop!!!") return } } }(ticker) return ch }
func main() { ch := Watch() time.Sleep(5 * time.Second) ch <- struct{}{} close(ch) } |
这段代码实现了一个简单的计时器功能,其中 Watch 函数返回一个用于控制计时器的通道 ch。让我解释一下代码的执行流程:
- 调用 Watch 函数,启动计时器 goroutine。
- 主 goroutine 休眠 5 秒。
- 主 goroutine 向通道 ch 发送消息,并关闭通道。
- 计时器 goroutine 接收到来自通道 ch 的消息,输出 "Ticker stop!!!" 并结束。
|
5.8 协程池
协程池是一种用于管理和复用协程(goroutine)的机制,可以有效地控制并发执行的协程数量,防止系统资源被过度消耗。
一个典型的协程池通常包含以下几个关键组件:
- 协程池管理器(Pool Manager):负责创建、管理和调度协程。
- 协程队列(Coroutine Queue):用于存放待执行的任务。
- 工作者协程(Worker Coroutine):实际执行任务的协程。
- 信号通道(Signal Channel):用于协程之间的通信,比如传递任务、控制协程状态等。
在使用协程池时,一般会按照以下步骤进行操作:
- 初始化协程池,设置最大协程数量、任务队列等参数。
- 启动协程池管理器,开始监控任务队列和工作者协程的状态。
- 将任务提交给协程池,由协程池管理器根据设定的规则分配任务给空闲的工作者协程执行。
- 工作者协程执行任务,并在执行完任务后返回到协程池中,等待下一个任务分配。
通过合理使用协程池,可以避免因为大量协程同时运行而导致系统资源耗尽的问题,提高并发处理能力并优化系统性能。
Go package main
import ( "fmt" "sync" "sync/atomic" "time" )
type Task struct { f func() error }
func NewTask(funcArg func() error) *Task { return &Task{ f: funcArg, } }
type Pool struct { RunningWorkers int64 Capacity int64 JobCh chan *Task sync.Mutex }
func NewPool(capacity int64, taskNum int) *Pool { return &Pool{ Capacity: capacity, JobCh: make(chan *Task, taskNum), } } func (p *Pool) GetCap() int64 { return p.Capacity }
func (p *Pool) incRunning() { atomic.AddInt64(&p.RunningWorkers, 1)
}
func (p *Pool) decRunning() { atomic.AddInt64(&p.RunningWorkers, -1)
} func (p *Pool) GetRunningWorkers() int64 { return atomic.LoadInt64(&p.RunningWorkers) } func (p *Pool) run() { p.incRunning() go func() { defer func() { p.decRunning() }() for task := range p.JobCh { task.f() } }() }
func (p *Pool) AddTask(task *Task) { p.Lock() defer p.Unlock()
if p.GetRunningWorkers() < p.GetCap() { p.run() } p.JobCh <- task } func main() { pool := NewPool(3, 10)
for i := 0; i < 20; i++ { pool.AddTask(NewTask(func() error { fmt.Println("I am a task!") return nil })) }
time.Sleep(1e9) } |
当执行 main 函数时,会按照以下步骤进行:
- 创建一个容量为 3 的协程池 pool。
- 进入 for 循环,循环次数为 20,即要添加 20 个任务到协程池中。
- 每次循环都会调用 pool.AddTask 方法,向协程池中添加一个新的任务。该任务是通过 NewTask 函数创建的,其中包含一个匿名函数,该匿名函数输出 "I am a task!"。
- 在 AddTask 方法中,会先获取当前运行的工作者协程数量,如果小于协程池的容量(这里是 3),则会启动一个新的工作者协程来执行任务。否则,任务会被放入任务通道中等待执行。
- 每个任务被执行时,会输出 "I am a task!",表示任务正在执行。
- 主 goroutine 中的 time.Sleep(1e9) 让程序暂停 1 秒钟,以确保所有任务有足够的时间执行。
- 在时间结束后,主 goroutine 结束运行,程序结束。
|
5.9 反射
- 反射就是程序在运行时能够检测自身和修改自身的一种能力
Go 语言中,反射是一种强大的工具,可以让程序在运行时检查和操作变量、接口、结构体等数据类型。Go 的反射包 reflect 提供了一组函数,可以用来实现反射操作。通过反射,可以做到以下几种操作:
- 获取类型信息:可以通过反射获取一个变量的类型信息,包括类型名、字段信息、方法信息等。
- 创建实例:可以通过反射动态创建一个类型的实例。
- 获取和设置字段值:可以通过反射获取和设置结构体中字段的值。
- 调用方法:可以通过反射调用结构体中的方法。
- 判断类型:可以判断一个变量的类型是否符合预期。
需要注意的是,由于反射是在运行时进行的,因此会带来一定的性能损耗。在使用反射时,需要谨慎考虑性能和代码可读性之间的平衡。
常用函数和方法
在 Go 语言的 reflect 包中,有一些常用的函数和方法可以用来进行反射操作。下面列举了一些常见的函数和方法:
TypeOf:获取一个接口值的类型信息。
Go func TypeOf(i interface{}) Type |
ValueOf:获取一个接口值的 reflect.Value 对象。
Go func ValueOf(i interface{}) Value |
Elem:获取指针、数组、切片、字典或通道类型的基础元素类型。
Go func (v Value) Elem() Value |
FieldByName:根据字段名获取结构体的字段值。
Go func (v Value) FieldByName(name string) Value |
NumField:获取结构体类型的字段数量。
Go func (t Type) NumField() int |
MethodByName:根据方法名获取结构体的方法值。
Go func (v Value) MethodByName(name string) Value |
Call:调用函数或方法。
Go func (v Value) Call(in []Value) []Value |
SetInt、SetFloat、SetString 等:设置整数、浮点数、字符串等类型的值。
Go func (v Value) SetInt(x int64) func (v Value) SetFloat(x float64) func (v Value) SetString(x string) // 其他类型的设置值的方法类似 |
获取类型信息:
Go package main
import ("fmt" "reflect" )
func main() {var num int = 10 fmt.Println(reflect.TypeOf(num)) // 输出变量 num 的类型信息 } |
创建实例:
Go package main
import ("fmt" "reflect" )
type Person struct { Name string Age int }
func main() { pType := reflect.TypeOf(Person{}) pValue := reflect.New(pType).Elem() pValue.FieldByName("Name").SetString("Alice") pValue.FieldByName("Age").SetInt(30) fmt.Println(pValue.Interface().(Person)) // 输出动态创建的 Person 实例 } |
获取和设置字段值:
Go package main
import ("fmt""reflect" )
type User struct { Name string Age int }
func main() { user := User{Name: "Bob", Age: 25} uValue := reflect.ValueOf(user) fmt.Println(uValue.FieldByName("Name").Interface()) // 获取字段 Name 的值 uValue.FieldByName("Age").SetInt(30) // 设置字段 Age 的值 fmt.Println(user) // 输出修改后的 User 结构体 } |
调用方法:
Go package main
import ("fmt" "reflect" )
type Calculator struct{}
func (c Calculator) Add(a, b int) int {return a + b }
func main() { calculator := Calculator{} cValue := reflect.ValueOf(calculator) method := cValue.MethodByName("Add") args := []reflect.Value{reflect.ValueOf(10), reflect.ValueOf(20)} result := method.Call(args) fmt.Println(result[0].Int()) // 输出调用 Add 方法的结果 } |