文章目录
- Goroutine
- Sync(同步机制)
- sync/atomic(原子操作函数)
-
- 原子加载和存储(Load/Store)
- 原子加法和减法(Add)
- [原子比较并交换(CompareAndSwap,简称 CAS)](#原子比较并交换(CompareAndSwap,简称 CAS))
- 原子交换(Swap)
- Context(上下文)
- Channels
- Select
- gpool(协程池库)
文章中的示例均在 go1.22.3 版本上运行。
Goroutine
Goroutine是由Go语言提供的一种轻量级线程(协程)管理方式。它比操作系统的线程更加高效和轻量,因此可以在同一时间运行成千上万个Goroutine。
Goroutine是实现并发编程的核心。它们是轻量级线程(协程),由Go运行时管理,可以在同一时间运行多个Goroutine,从而实现高并发处理。
-
特性
- 轻量级: Goroutine非常轻量,每个Goroutine消耗的内存远小于一个系统线程。
- 独立栈: 每个Goroutine有一个独立的栈,初始大小通常是很小的(如几KB),并且可以根据需要动态增长。
- 非抢占式调度: Go运行时会负责调度Goroutine,使用了协作式调度和一小部分抢占式调度。Go1.14版本之后引入异步抢占机制,使得长时间运行的Goroutine可以被抢占,从而提高系统响应性。
- Race Detector: 提供了竞争检测工具,帮助开发者发现并解决数据竞争问题。
- 编译器/解释器/虚拟机层面的多任务
- 多个轻量级线程(协程)可能在一个或多个系统线程上运行
-
调度器(Goroutine 可能的切换点)
- I/O, select
- channel
- 等待锁
- 函数调用(有时)
- runtime.Gosched() :交出控制权,以允许其他轻量级线程(协程)运行
-
优势
- 高并发性:Goroutine允许在同一时间处理大量任务,提高了程序的并发能力。
- 简单的并发模型:相比于传统的多线程编程,Goroutine和Channel提供了一个更简单和安全的并发编程模型。
- 自动管理:Go运行时自动管理Goroutine的调度和内存分配,使得开发者不需要过多关注底层实现细节。
-
启动 Goroutine
go
package main
import (
"fmt"
"time"
)
func say(s string) {
fmt.Println(s)
}
func main() {
// go 关键字用于启动一个新的 Goroutine,用于并发执行函数或方法。
go say("Hello World")
// 让主Goroutine暂停执行1秒钟
time.Sleep(1 * time.Second)
}
如果主Goroutine在启动其他Goroutine之后立即结束,其他Goroutine可能没有机会完成其工作。使用time.Sleep来延迟主Goroutine的结束,可以让其他Goroutine有足够的时间完成其任务。
- 启动匿名函数的 Goroutine
go
package main
import (
"fmt"
"time"
)
func main() {
go func(s string) {
fmt.Println(s)
}("Hello World")
time.Sleep(1 * time.Second)
}
- 启动方法的 Goroutine
go
package main
import (
"fmt"
"time"
)
type Person struct {
Name string
Age int
}
func (person *Person) sayHello() {
fmt.Println("Hello, my name is", person.Name)
}
func main() {
person := &Person{Name: "John Doe", Age: 18}
go person.sayHello()
time.Sleep(1 * time.Second)
}
- Race Detector(数据竞争检测器) 是一个用于检测数据竞争的工具。数据竞争是在两个或多个goroutine同时访问相同的变量,并且至少有一个是写操作而没有同步机制保护的情况下发生的。
go
package main
import (
"fmt"
"time"
)
var counter int
func increment() {
counter++
}
func main() {
for i := 0; i < 10; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println("Final counter:", counter) // 输出:Final counter: 10
}
bash
# 执行后输出数据竞争的警告
go run -race main.go
注意: 修复数据竞争问题,可以使用 同步机制,例如sync.Mutex或sync/atomic包。
Sync(同步机制)
- sync.Mutex(互斥锁)
- sync.RWMutex(读写互斥锁)
- sync.WaitGroup(等待组)
- sync.Map(并发安全的映射)
sync.Mutex(互斥锁)
用于在多Goroutine环境中保护共享资源,防止竞态条件(Race Condition)的发生。互斥锁通过锁和解锁的机制来确保同时只有一个Goroutine可以访问共享资源。
- 避免死锁:确保锁和解锁成对出现,尤其是在可能出现错误或提前返回的代码路径上使用defer来确保解锁。
- 最小化锁的粒度:锁定的范围应该尽可能小,以减少锁的持有时间,从而提高并发性能。
- 避免重复加锁:在同一个Goroutine内重复锁定同一个互斥锁会导致死锁。
go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
var counter int
// 启动多个Goroutine,每个都增加计数器
for i := 0; i < 2; i++ {
go func(id int) {
for j := 0; j < 4; j++ {
mu.Lock() // 加锁
counter++
fmt.Printf("Goroutine %d incremented counter to %d\n", id, counter)
mu.Unlock() // 解锁
time.Sleep(time.Millisecond * 100)
}
}(i)
}
// 等待一段时间以确保所有Goroutine完成
time.Sleep(2 * time.Second)
fmt.Printf("Final counter value: %d\n", counter)
}
- 输出结果
bash
Goroutine 0 incremented counter to 1
Goroutine 1 incremented counter to 2
Goroutine 1 incremented counter to 3
Goroutine 0 incremented counter to 4
Goroutine 1 incremented counter to 5
Goroutine 0 incremented counter to 6
Goroutine 0 incremented counter to 7
Goroutine 1 incremented counter to 8
Final counter value: 8
sync.RWMutex(读写互斥锁)
允许多个读操作并发执行,同时写操作是独占的,即在写操作进行时,不允许任何读操作或其他写操作。读写互斥锁的主要优点是在读多写少的情况下,可以提高程序的并发性能。
- Lock(): 锁定写操作。
- Unlock(): 解锁写操作。
- RLock(): 锁定读操作。
- RUnlock(): 解锁读操作。
go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.RWMutex
var counter int
// 启动多个Goroutine进行读操作
for i := 0; i < 2; i++ {
go func(id int) {
for j := 0; j < 2; j++ {
mu.RLock() // 加锁读操作
fmt.Printf("Goroutine %d read counter: %d\n", id, counter)
mu.RUnlock() // 解锁读操作
time.Sleep(time.Millisecond * 100)
}
}(i)
}
// 启动多个Goroutine进行写操作
for i := 0; i < 2; i++ {
go func(id int) {
for j := 0; j < 2; j++ {
mu.Lock() // 加锁写操作
counter++
fmt.Printf("Goroutine %d incremented counter to: %d\n", id, counter)
mu.Unlock() // 解锁写操作
time.Sleep(time.Millisecond * 150)
}
}(i)
}
// 等待一段时间以确保所有Goroutine完成
time.Sleep(3 * time.Second)
fmt.Printf("Final counter value: %d\n", counter)
}
- 输出结果
bash
Goroutine 1 read counter: 0
Goroutine 1 incremented counter to: 1
Goroutine 0 read counter: 1
Goroutine 0 incremented counter to: 2
Goroutine 0 read counter: 2
Goroutine 1 read counter: 2
Goroutine 0 incremented counter to: 3
Goroutine 1 incremented counter to: 4
Final counter value: 4
sync.WaitGroup(等待组)
用于等待一组 Goroutine 完成。当我们需要启动多个 Goroutine 并等待它们全部完成时,sync.WaitGroup 非常有用。WaitGroup 提供了一种简单的方式来计数并发操作,并在所有操作完成后继续执行。
- Add(delta int): 增加或减少等待计数器的值。
- Done(): 将等待计数器的值减一。通常在 Goroutine 完成时调用。
- Wait(): 阻塞直到等待计数器变为零。
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 在函数结束时调用 Done,减少计数器
fmt.Printf("Worker %d starting\n", id)
// 模拟工作
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加计数器
go worker(i, &wg)
}
wg.Wait() // 阻塞直到所有 Goroutine 完成
fmt.Println("All workers done")
}
- 输出结果
bash
Worker 5 starting
Worker 2 starting
Worker 3 starting
Worker 1 starting
Worker 4 starting
Worker 4 done
Worker 1 done
Worker 2 done
Worker 5 done
Worker 3 done
All workers done
sync.Map(并发安全的映射)
它提供了一种简单且高效的方式来在多个 Goroutine 之间共享数据,而无需显式地使用互斥锁来保护对 map 的访问。与普通的 Go map 不同,sync.Map 进行了优化,特别适用于读多写少的场景。它提供了原子操作来确保并发访问的安全性,同时避免了手动管理锁的复杂性。
- Store(key, value interface{}): 设置指定 key 的值。
- Load(key interface{}) (value interface{}, ok bool): 获取指定 key 的值。
- LoadOrStore(key, value interface{}) (actual interface{}, loaded bool): 获取指定 key 的值,如果 key 不存在则存储指定的值。
- Delete(key interface{}): 删除指定 key 的值。
- Range(f func(key, value interface{}) bool): 遍历 map 中的所有 key 和 value。
go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var m sync.Map
var wg sync.WaitGroup
// 启动多个 Goroutine 进行写操作
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
key := fmt.Sprintf("key-%d-%d", id, j)
value := fmt.Sprintf("value-%d-%d", id, j)
m.Store(key, value)
fmt.Printf("Goroutine %d stored %s: %s\n", id, key, value)
time.Sleep(time.Millisecond * 100)
}
}(i)
}
// 启动多个 Goroutine 进行读操作
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
key := fmt.Sprintf("key-%d-%d", id, j)
if value, ok := m.Load(key); ok {
fmt.Printf("Goroutine %d loaded %s: %s\n", id, key, value)
} else {
fmt.Printf("Goroutine %d failed to load %s\n", id, key)
}
time.Sleep(time.Millisecond * 150)
}
}(i)
}
wg.Wait() // 等待所有 Goroutine 完成
// 遍历 map 中的所有键值对
m.Range(func(key, value interface{}) bool {
fmt.Printf("Final key: %s, value: %s\n", key, value)
return true
})
}
- 输出结果
bash
Goroutine 1 failed to load key-1-0
Goroutine 0 failed to load key-0-0
Goroutine 0 stored key-0-0: value-0-0
Goroutine 1 stored key-1-0: value-1-0
Goroutine 0 stored key-0-1: value-0-1
Goroutine 1 stored key-1-1: value-1-1
Goroutine 0 loaded key-0-1: value-0-1
Goroutine 1 loaded key-1-1: value-1-1
Final key: key-0-0, value: value-0-0
Final key: key-1-0, value: value-1-0
Final key: key-0-1, value: value-0-1
Final key: key-1-1, value: value-1-1
sync/atomic(原子操作函数)
在并发编程中,使用原子操作可以有效地避免竞争条件和数据不一致问题。Go 语言标准库中的 sync/atomic 包提供了一组用于原子操作的函数,这些函数可以对整数和指针类型进行原子读写和修改操作。
原子加载和存储(Load/Store)
- atomic.LoadInt32 和 atomic.StoreInt32
- atomic.LoadInt64 和 atomic.StoreInt64
- atomic.LoadUint32 和 atomic.StoreUint32
- atomic.LoadUint64 和 atomic.StoreUint64
- atomic.LoadPointer 和 atomic.StorePointer
go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var value int64
var wg sync.WaitGroup
// 启动一个 Goroutine 来定期更新 value
wg.Add(1)
go func() {
defer wg.Done()
for i := int64(0); i < 2; i++ {
atomic.StoreInt64(&value, i)
fmt.Printf("Stored value: %d\n", i)
time.Sleep(time.Millisecond * 100)
}
}()
// 启动一个 Goroutine 来定期读取 value
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 2; i++ {
v := atomic.LoadInt64(&value)
fmt.Printf("Loaded value: %d\n", v)
time.Sleep(time.Millisecond * 150)
}
}()
wg.Wait() // 等待所有 Goroutine 完成
}
- 输出结果
bash
Loaded value: 0
Stored value: 0
Stored value: 1
Loaded value: 1
原子加法和减法(Add)
- atomic.AddInt32
- atomic.AddInt64
- atomic.AddUint32
- atomic.AddUint64
go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var counter int64
var wg sync.WaitGroup
// 启动多个 Goroutine 进行增加操作
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
atomic.AddInt64(&counter, 1)
fmt.Printf("Goroutine %d added 1, counter: %d\n", id, atomic.LoadInt64(&counter))
time.Sleep(time.Millisecond * 10)
}
}(i)
}
// 启动多个 Goroutine 进行减少操作
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
atomic.AddInt64(&counter, -1)
fmt.Printf("Goroutine %d subtracted 1, counter: %d\n", id, atomic.LoadInt64(&counter))
time.Sleep(time.Millisecond * 20)
}
}(i)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Printf("Final counter value: %d\n", counter)
}
- 输出结果
bash
Goroutine 1 subtracted 1, counter: -1
Goroutine 0 subtracted 1, counter: 0
Goroutine 1 added 1, counter: 1
Goroutine 0 added 1, counter: 0
Goroutine 0 added 1, counter: 1
Goroutine 1 added 1, counter: 2
Goroutine 1 subtracted 1, counter: 1
Goroutine 0 subtracted 1, counter: 0
Final counter value: 0
原子比较并交换(CompareAndSwap,简称 CAS)
- atomic.CompareAndSwapInt32
- atomic.CompareAndSwapInt64
- atomic.CompareAndSwapUint32
- atomic.CompareAndSwapUint64
- atomic.CompareAndSwapPointer
go
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func main() {
var counter int64
var wg sync.WaitGroup
// 启动多个 Goroutine 进行增加操作
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
for {
current := atomic.LoadInt64(&counter)
next := current + 1
if atomic.CompareAndSwapInt64(&counter, current, next) {
fmt.Printf("Goroutine %d added 1, counter: %d\n", id, next)
break
}
}
time.Sleep(time.Millisecond * 10)
}
}(i)
}
// 启动多个 Goroutine 进行减少操作
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 2; j++ {
for {
current := atomic.LoadInt64(&counter)
next := current - 1
if atomic.CompareAndSwapInt64(&counter, current, next) {
fmt.Printf("Goroutine %d subtracted 1, counter: %d\n", id, next)
break
}
}
time.Sleep(time.Millisecond * 20)
}
}(i)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Printf("Final counter value: %d\n", counter)
}
- 输出结果
bash
Goroutine 1 subtracted 1, counter: 0
Goroutine 0 added 1, counter: 1
Goroutine 1 added 1, counter: 1
Goroutine 0 subtracted 1, counter: 0
Goroutine 1 added 1, counter: 1
Goroutine 0 added 1, counter: 2
Goroutine 0 subtracted 1, counter: 1
Goroutine 1 subtracted 1, counter: 0
Final counter value: 0
原子交换(Swap)
- atomic.SwapInt32
- atomic.SwapInt64
- atomic.SwapUint32
- atomic.SwapUint64
- atomic.SwapPointer
go
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var value int32 = 10
var newValue int32 = 20
fmt.Printf("Original value: %d\n", value)
// 使用原子交换将 value 的值替换为 newValue
oldValue := atomic.SwapInt32(&value, newValue)
fmt.Printf("New value: %d, Old value: %d\n", value, oldValue)
}
- 输出结果
bash
Original value: 10
New value: 20, Old value: 10
Context(上下文)
context包提供了一种在函数间传递取消信号、超时和截止时间的方式。它主要用于控制多个Goroutine之间的取消、超时和截止操作。context包的核心类型是Context接口,它定义了一些方法来获取和设置上下文中的值、取消操作以及阻塞等待操作。
Context接口定义
go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline()方法返回上下文的截止时间(如果有),ok值表示是否有截止时间。如果上下文不支持截止时间,则ok为false。
- Done()方法返回一个<-chan struct{}类型的通道。当上下文被取消或者超时时,该通道将被关闭。
- Err()方法返回取消的原因。如果上下文没有被取消,则返回nil。
- Value(key)方法返回上下文中与key相关联的值,如果没有与key相关联的值,则返回nil。
创建Context
- context.Background():返回一个空的Context,它在没有明确指定的情况下作为所有其他上下文的父上下文。
- context.TODO():返回一个空的Context,它表示不应该使用的上下文,用于作为函数的参数传递,而不是nil。
- context.WithCancel(parent):返回一个具有取消功能的上下文,当调用cancel函数时,它将取消与之关联的上下文及其所有子上下文。
- context.WithTimeout(parent, timeout):返回一个具有截止时间的上下文,当指定的时间过去后,它将自动取消与之关联的上下文及其所有子上下文。
- context.WithDeadline(parent, deadline):类似于WithTimeout,不同之处在于它接受一个截止时间而不是持续时间。
- context.WithValue(parent, key, value):返回一个具有与key关联的值的上下文。这对于传递请求范围的参数很有用。
使用Context
go
package main
import (
"context"
"fmt"
"time"
)
// worker 函数代表一个工作任务的执行者。
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done(): // 监听上下文的取消信号
fmt.Printf("Worker %d: Context canceled\n", id)
return // 收到取消信号后退出Goroutine
default:
fmt.Printf("Worker %d: Working...\n", id)
time.Sleep(1 * time.Second) // 模拟工作时间
}
}
}
func main() {
// 创建一个带有3秒超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // 在main函数结束时取消上下文
// 启动多个Goroutine进行工作
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// 等待一段时间,然后取消上下文
time.Sleep(2 * time.Second)
cancel() // 取消上下文,通知所有Goroutine停止工作
// 等待一段时间,以观察效果
time.Sleep(2 * time.Second)
fmt.Println("Main program finished")
}
- 输出结果
bash
Worker 3: Working...
Worker 1: Working...
Worker 2: Working...
Worker 2: Working...
Worker 1: Working...
Worker 3: Working...
Worker 1: Context canceled
Worker 2: Context canceled
Worker 3: Context canceled
Main program finished
Channels
通道(Channel)是一种用来在Goroutine之间传递数据的方式。它是一种类型安全的、并发安全的、支持同步操作的数据结构。通道提供了一种通信的机制,允许一个Goroutine将数据发送到通道,另一个Goroutine从该通道接收数据。
注意: 使用通道的双向性可以更清楚地表达通道的用途。chan<- 向通道发送数据,<-chan 从通道接收数据。
通道的特性
- 类型安全: 通道是类型安全的,即通道在创建时必须指定数据元素的类型,只能发送和接收相同类型的数据。
- 并发安全: 通道在多个Goroutine之间进行通信时是并发安全的,无需额外的锁。
- 阻塞等待: 当通道为空时,试图从通道中接收数据的操作会阻塞当前Goroutine,直到有数据可用;当通道已满时,试图向通道发送数据的操作也会阻塞当前Goroutine,直到有空间可用。
- 同步操作: 通道提供了同步操作的机制,发送操作和接收操作都是同步的。
创建和使用通道
可以使用make函数来创建通道,并使用<-操作符来发送和接收数据。
go
package main
import "fmt"
func main() {
// 创建一个int类型的通道
ch := make(chan int)
// 启动一个Goroutine发送数据到通道
go func() {
ch <- 42 // 将数据42发送到通道
}()
// 从通道接收数据并打印
value := <-ch // 从通道接收数据并赋值给变量value
fmt.Println(value) // 输出:42
}
通道的关闭
通道还支持关闭操作,可以使用close函数来关闭通道。关闭通道后,不能再向其发送数据,但仍然可以从中接收数据。关闭的通道会立即释放其所有资源。
go
package main
import "fmt"
func main() {
ch := make(chan int)
// 启动一个Goroutine发送数据到通道
go func() {
defer close(ch) // 在Goroutine结束前关闭通道
ch <- 42 // 将数据42发送到通道
}()
// 从通道接收数据并打印
for value := range ch { // 循环接收通道数据,直到通道关闭
fmt.Println(value) // 输出42
}
}
通道作为函数参数
当将通道作为函数参数传递时,可以将通道作为参数传递给函数,并在函数内部对该通道进行操作。
go
package main
import "fmt"
// send 函数用于向通道发送数据
func send(ch chan<- int, value int) {
ch <- value // 向通道发送数据
}
// receive 函数用于从通道接收数据
func receive(ch <-chan int) {
value := <-ch // 从通道接收数据
fmt.Println("Received:", value)
}
func main() {
// 创建一个int类型的通道
ch := make(chan int)
// 启动一个Goroutine发送数据到通道
go send(ch, 42)
// 在主Goroutine中从通道接收数据
receive(ch)
}
- 输出结果
bash
Received: 42
通道作为函数的返回值
将通道作为函数的返回值通常用于在不同的Goroutine之间进行通信。
go
package main
import "fmt"
// createChannel 函数用于创建并返回一个int类型的通道
func createChannel() <-chan int {
ch := make(chan int) // 创建一个int类型的通道
// 启动一个Goroutine发送数据到通道
go func() {
defer close(ch) // 在Goroutine结束前关闭通道
ch <- 42 // 向通道发送数据
}()
return ch // 返回通道
}
// receiveData 函数用于从通道接收数据
func receiveData(ch <-chan int) {
value := <-ch
fmt.Println("Received:", value) // 输出Received: 42
}
func main() {
// 调用createChannel函数创建并返回通道
ch := createChannel()
// 在主Goroutine中从通道接收数据
receiveData(ch)
}
- 输出结果
bash
Received: 42
注意: 使用通道的双向性可以更清楚地表达通道的用途。chan<- 向通道发送数据,<-chan 从通道接收数据。
结构体类型的通道示例
go
package main
import "fmt"
// 定义一个结构体
type Person struct {
Name string
Age int
}
// PersonChannel 结构体封装了一个传递 Person 结构体的通道
type PersonChannel struct {
ch chan Person
}
// NewPersonChannel 函数用于创建并返回一个只接收 Person 结构体的通道
func NewPersonChannel() <-chan Person {
ch := make(chan Person)
// 启动一个Goroutine发送数据到通道
go func() {
defer close(ch) // 在Goroutine结束前关闭通道
ch <- Person{Name: "Alice", Age: 30} // 向通道发送 Person 结构体
}()
return ch
}
// ReceiveDataFromChannel 函数接收一个只接收 Person 结构体的通道,并从该通道接收数据
func ReceiveDataFromChannel(ch <-chan Person) {
person := <-ch // 从通道接收 Person 结构体
fmt.Printf("Received: %+v\n", person) // 输出Received: {Name:Alice Age:30}
}
func main() {
// 创建一个只接收 Person 结构体的通道
ch := NewPersonChannel()
// 在主Goroutine中从通道接收数据
ReceiveDataFromChannel(ch)
}
- 输出结果
bash
Received: {Name:Alice Age:30}
Select
select语句用于在多个通道操作中进行选择。它类似于switch语句,但用于通道操作。select会阻塞,直到其中一个case中的操作可以进行,然后执行该操作。如果有多个通道可以操作,则随机选择一个执行。select主要用于处理多路通道操作,以实现超时、非阻塞发送/接收以及处理多个通道的场景。
- select 会选择一个可以立即执行的 case 语句。如果有多个 case 语句可以执行,会随机选择一个执行。
- 如果没有任何 case 语句可以执行,且有 default 语句,则执行 default 语句。
- 如果没有任何 case 语句可以执行,且没有 default 语句,则 select 会阻塞,直到有一个 case 语句可以执行。
select 语法
go
select {
case <-ch1:
// 从 ch1 接收到了数据
case ch2 <- x:
// 向 ch2 发送了数据 x
case <-time.After(time.Second):
// 超时,执行超时逻辑
default:
// 如果没有任何通道操作可以立即进行,则执行这里的逻辑
}
从通道中接收数据
go
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
}
- 应用场景: 当你有一个 Goroutine 在等待从通道 ch1 中接收数据时,可以使用这种方式。例如,一个服务器 Goroutine 从多个客户端连接通道中接收消息。
go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Message from ch1"
}()
select {
case msg := <-ch1:
fmt.Println("Received:", msg)
}
}
- 输出结果
bash
Received: Message from ch1
向通道发送数据
go
select {
case ch2 <- x:
fmt.Println("Sent to ch2:", x)
}
- 应用场景: 当你需要在 Goroutine 中向通道发送数据时,可以使用这种方式。例如,一个任务分配器向工作者通道发送任务。
go
package main
import "fmt"
func main() {
ch2 := make(chan int)
go func() {
select {
case ch2 <- 42:
fmt.Println("Sent 42 to ch2")
}
}()
// 接收数据以避免死锁
fmt.Println("Received from ch2:", <-ch2)
}
- 输出结果
bash
Received from ch2: 42
超时处理
go
select {
case <-time.After(time.Second):
fmt.Println("Timeout after 1 second")
}
- 应用场景: 当你希望在一定时间内等待某些操作完成,如果超时则执行其他操作时,可以使用这种方式。例如,网络请求超时处理。
go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "Message"
}()
select {
case msg := <-ch:
fmt.Println("Received:", msg)
case <-time.After(1 * time.Second):
fmt.Println("Timeout after 1 second")
}
}
- 输出结果
bash
Timeout after 1 second
非阻塞操作
go
select {
default:
fmt.Println("No channel operations ready")
}
- 应用场景: 当你希望在没有任何通道操作可以立即进行时执行某些逻辑,可以使用这种方式。例如,检查通道是否有数据可接收或发送,但不希望阻塞。
go
package main
import "fmt"
func main() {
ch := make(chan int, 1)
select {
case ch <- 1:
fmt.Println("Sent to ch")
default:
fmt.Println("No channel operations ready")
}
select {
case msg := <-ch:
fmt.Println("Received from ch:", msg)
default:
fmt.Println("No channel operations ready")
}
}
- 输出结果
bash
Sent to ch
Received from ch: 1
综合应用场景
go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan int)
// 启动一个Goroutine,在1秒后向ch1发送数据
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Hello from ch1"
}()
// 启动一个Goroutine,在2秒后向ch2发送数据
go func() {
time.Sleep(2 * time.Second)
ch2 <- 42
}()
// 创建一个3秒的超时通道
timeout := time.After(3 * time.Second)
// 循环4次以涵盖所有操作,包括非阻塞和超时处理
for i := 0; i < 4; i++ {
select {
case msg := <-ch1:
// 从ch1接收到数据并输出
fmt.Printf("Received from ch1: %s\n", msg)
case val := <-ch2:
// 从ch2接收到数据并输出
fmt.Printf("Received from ch2: %d\n", val)
case <-timeout:
// 如果所有通道操作在3秒内未完成,执行超时处理并退出循环
fmt.Println("Timeout: No more data to receive")
return
default:
// 如果没有任何通道操作可以立即进行,输出提示信息
fmt.Println("No channel operations ready")
}
// 模拟其他处理逻辑
time.Sleep(500 * time.Millisecond)
}
// 循环结束后输出完成信息
fmt.Println("Finished processing all channel operations")
}
- 输出结果
bash
No channel operations ready
No channel operations ready
Received from ch1: Hello from ch1
No channel operations ready
Finished processing all channel operations
gpool(协程池库)
字节跳动的 gpool 是一个高效的 Goroutine 池,用于管理和调度 Go 语言中的并发任务。gpool 提供了一种机制,通过复用 Goroutine 来减少创建和销毁 Goroutine 的开销,从而优化性能和资源使用。
- 高效的任务调度: gpool 能够高效地调度任务,确保在高并发场景下,任务能够被及时处理。
- Goroutine 复用: 通过复用 Goroutine,减少频繁创建和销毁 Goroutine 带来的开销,提升系统的整体性能。
- 灵活的配置: 提供了多种配置选项,可以根据具体的业务场景调整 Goroutine 池的大小和行为。
- 任务队列: 任务被提交到一个队列中,由 Goroutine 池中的 Goroutine 按顺序执行。
- 错误处理: 支持任务执行中的错误处理机制,确保系统的稳定性。
安装使用
bash
go get github.com/bytedance/gopkg/util/gopool
go
package main
import (
"fmt"
"sync"
"time"
"github.com/bytedance/gopkg/util/gopool"
)
func main() {
// 定义一个任务函数
task := func(i int) {
fmt.Printf("Processing task: %d\n", i)
time.Sleep(time.Second) // 模拟任务处理时间
fmt.Printf("Completed task: %d\n", i)
}
// 创建一个具有5个worker的协程池
pool := gopool.NewPool("examplePool", 5, gopool.NewConfig())
var wg sync.WaitGroup
// 提交10个任务到协程池
for i := 0; i < 10; i++ {
wg.Add(1)
i := i // 避免闭包问题
pool.Go(func() {
defer wg.Done()
task(i)
})
}
// 等待所有任务完成
wg.Wait()
fmt.Println("All tasks completed.")
}
- 输出结果
bash
Processing task: 0
Processing task: 1
Processing task: 3
Processing task: 2
Processing task: 4
Completed task: 1
Processing task: 5
Completed task: 4
Completed task: 2
Processing task: 7
Completed task: 0
Processing task: 8
Processing task: 6
Completed task: 3
Processing task: 9
Completed task: 5
Completed task: 6
Completed task: 9
Completed task: 7
Completed task: 8
All tasks completed.