关注【索引目录】服务号,更多精彩内容等你来探索!
前言
在 Go 编程语言中,Goroutines 和 Channels 是并发编程中必不可少的概念。它们有助于解决各种与并发相关的问题。本文重点介绍select,它是协调多个 Channels 的桥梁。
简介select
什么是select
select是 Go 语言中的一种控制结构,用于在多个通信操作中选择可执行的操作。它协调多个通道上的读写操作,实现跨通道的非阻塞数据传输、同步和控制。
为什么我们需要select
Go 中的语句select提供了一种多路复用通道的机制。它允许我们在多个通道上等待并处理消息。相比于简单地使用for循环来迭代通道,select这是一种更高效的管理多个通道的方法。
以下是一些常见的使用场景select:
- 等待多个通道的消息(多路复用)
当需要等待多个通道的消息时, select可以方便的等待其中任何一个通道接收数据,避免使用多个Goroutines进行同步和等待。 - 频道消息超时等待
当我们需要在特定的时间段内等待某个频道的消息时, select可以结合该time包来实现定时等待。 - 通道上的非阻塞读/写操作:
如果通道中没有数据或空间,则从通道读取或写入操作将阻塞。使用 select分支default可以实现非阻塞操作,避免死锁或无限循环。
因此,的主要目的select是提供一种高效、易用的机制来处理多通道,简化Goroutine的同步和等待,使程序更具可读性、高效性和可靠性。
基础知识select
句法
select {
case <- channel1:
// channel1 is ready
case data := <- channel2:
// channel2 is ready, and data can be read
case channel3 <- data:
// channel3 is ready, and data can be written into it
default:
// no channel is ready
}
这里,<- channel1表示从 读取channel1,data := <- channel2表示将数据接收到data。channel3 <- data表示data写入channel3。
的语法select与 类似switch,但它专用于通道操作。在一个select语句中,我们可以定义多个case块,每个块代表一个用于读取或写入数据的通道操作。如果多个分支同时就绪,则会随机选择一个。如果没有分支就绪,default则执行分支(如果有);否则,select将会阻塞,直到至少一个分支就绪。
基本用法
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
time.Sleep(1 * time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- 2
}()
for i := 0; i < 2; i++ {
select {
case data, ok := <-ch1:
if ok {
fmt.Println("Received from ch1:", data)
} else {
fmt.Println("Channel closed")
}
case data, ok := <-ch2:
if ok {
fmt.Println("Received from ch2:", data)
} else {
fmt.Println("Channel closed")
}
}
}
select {
case data, ok := <-ch1:
if ok {
fmt.Println("Received from ch1:", data)
} else {
fmt.Println("Channel closed")
}
case data, ok := <-ch2:
if ok {
fmt.Println("Received from ch2:", data)
} else {
fmt.Println("Channel closed")
}
default:
fmt.Println("No data received, default branch executed")
}
}
执行结果
Received from ch1: 1
Received from ch2: 2
No data received, default branch executed
在上面的例子中,创建了两个通道ch1和ch2。不同的 Goroutines 在不同的延迟后向这两个通道写入数据。主 Goroutine 使用一个select语句监听这两个通道。当数据到达一个通道时,它会打印数据。由于ch1比 先收到数据,因此会先打印ch2消息,然后再打印。"Received from ch1: 1""Received from ch2: 2"
为了演示default分支,程序包含第二个select代码块。此时, 和 都ch1为ch2空,因此default执行分支,并打印"No data received, default branch executed"。
场景组合select和渠道
实现超时控制
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)
ch <- 1
}()
select {
case data, ok := <-ch:
if ok {
fmt.Println("Received data:", data)
} else {
fmt.Println("Channel closed")
}
case <-time.After(2 * time.Second):
fmt.Println("Timed out!")
}
}
执行结果:超时!
在此示例中,程序ch在 3 秒后将数据发送到通道。但是,该select块设置了 2 秒的超时时间。如果在该时间内未收到任何数据,则会触发超时情况。
实现多任务并发控制
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
for i := 0; i < 10; i++ {
go func(id int) {
ch <- id
}(i)
}
for i := 0; i < 10; i++ {
select {
case data, ok := <-ch:
if ok {
fmt.Println("Task completed:", data)
} else {
fmt.Println("Channel closed")
}
}
}
}
执行结果(每次运行的顺序可能有所不同):
Task completed: 1
Task completed: 5
Task completed: 2
Task completed: 3
Task completed: 4
Task completed: 0
Task completed: 9
Task completed: 6
Task completed: 7
Task completed: 8
在此示例中,启动了 10 个 Goroutine 来并发执行任务。使用单个通道接收任务完成通知。主函数使用 监听此通道select,并在收到每个已完成的任务后进行处理。
收听多个频道
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// Start Goroutine 1 to send data to ch1
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
time.Sleep(time.Second)
}
}()
// Start Goroutine 2 to send data to ch2
go func() {
for i := 5; i < 10; i++ {
ch2 <- i
time.Sleep(time.Second)
}
}()
// Main Goroutine receives and prints data from ch1 and ch2
for i := 0; i < 10; i++ {
select {
case data := <-ch1:
fmt.Println("Received from ch1:", data)
case data := <-ch2:
fmt.Println("Received from ch2:", data)
}
}
fmt.Println("Done.")
}
执行结果(每次运行的顺序可能有所不同):
Received from ch2: 5
Received from ch1: 0
Received from ch1: 1
Received from ch2: 6
Received from ch1: 2
Received from ch2: 7
Received from ch1: 3
Received from ch2: 8
Received from ch1: 4
Received from ch2: 9
Done.
在此示例中,select启用了来自多个通道的数据复用。它允许程序同时监听ch1和ch2执行,而无需单独的 Goroutine 进行同步。
使用default实现非阻塞读写
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1)
go func() {
for i := 1; i <= 5; i++ {
ch <- i
time.Sleep(1 * time.Second)
}
close(ch)
}()
for {
select {
case val, ok := <-ch:
if ok {
fmt.Println(val)
} else {
ch = nil
}
default:
fmt.Println("No value ready")
time.Sleep(500 * time.Millisecond)
}
if ch == nil {
break
}
}
}
执行结果(每次运行的顺序可能有所不同):
No value ready
1
No value ready
2
No value ready
No value ready
3
No value ready
No value ready
4
No value ready
No value ready
5
No value ready
No value ready
这段代码使用default分支语句来实现非阻塞的通道读写操作。在select语句中,如果某个通道已准备好进行读写操作,则执行相应的分支。如果没有通道已准备好,则default执行分支语句,从而避免阻塞。
使用须知select
使用时请牢记以下几点select:
select语句只能用于通信操作,例如从通道读取或写入;它们不能用于普通计算或函数调用。 -
语句 select会阻塞,直到至少一个案例准备就绪。如果多个案例准备就绪,则随机选择一个。 -
如果没有准备好的案例并且 default存在分支,default则立即执行该分支。 -
当使用 中的通道时 select,请确保通道已正确初始化。 -
如果通道已关闭,仍然可以读取数据,直到其为空。从已关闭的通道读取数据会返回元素类型的零值以及一个指示通道关闭状态的布尔值。
综上所述,在使用时select,要仔细考虑每个case的条件和执行顺序,避免出现死锁等问题。
关注【索引目录】服务号,更多精彩内容等你来探索!

