一文带你学会Go select语句轻松实现高效并发
作者:陈明勇 发布时间:2024-05-22 10:29:50
前言
在 Go
语言中,Goroutine
和 Channel
是非常重要的并发编程概念,它们可以帮助我们解决并发编程中的各种问题。关于它们的基本概念和用法,前面的文章 一文初探 Goroutine 与 channel 中已经进行了介绍。而本文将重点介绍 select
,它是协调多个 channel
的桥梁。
select 介绍
什么是 select
select
是 Go
语言中的一种控制结构,用于在多个通信操作中选择一个可执行的操作。它可以协调多个 channel
的读写操作,使得我们能够在多个 channel
中进行非阻塞的数据传输、同步和控制。
为什么需要 select
Go
语言中的 select
语句是一种用于多路复用通道的机制,它允许在多个通道上等待并处理消息。相比于简单地使用 for
循环遍历通道,使用 select
语句能够更加高效地管理多个通道。
以下是一些 select
语句的使用场景:
1.等待多个通道的消息(多路复用)
当我们需要等待多个通道的消息时,使用 select
语句可以非常方便地等待这些通道中的任意一个通道有消息到达,从而避免了使用多个goroutine进行同步和等待。
2.超时等待通道消息
当我们需要在一段时间内等待某个通道有消息到达时,使用 select
语句可以与 time
包结合使用实现定时等待。
3.在通道上进行非阻塞读写
在使用通道进行读写时,如果通道没有数据,读操作或写操作将会阻塞。但是使用 select
语句结合 default
分支可以实现非阻塞读写,从而避免了死锁或死循环等问题。
因此,select
的主要作用是在处理多个通道时提供了一种高效且易于使用的机制,简化了多个 goroutine
的同步和等待,使程序更加可读、高效和可靠。
select 基础
语法
select {
case <- channel1:
// channel1准备好了
case data := <- channel2:
// channel2准备好了,并且可以读取到数据data
case channel3 <- data:
// channel3准备好了,并且可以往其中写入数据data
default:
// 没有任何channel准备好了
}
其中, <- channel1
表示读取 channel1
的数据,data <- channel2
表示用 data
去接收数据;channel3 <- data
表示往 channel3
中写入数据。
select
的语法形式类似于 switch
,但是它只能用于 channel
操作。在 select
语句中,我们可以定义多个 case
,每个 case
都是一个 channel
操作,用于读取或写入数据。如果有多个 case
同时可执行,则会随机选择其中一个。如果没有任何可执行的 case
,则会执行 default
分支(如果存在),或者阻塞等待直到至少有一个 case
可执行为止。
基本用法
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("从 ch1 接收到数据:", data)
} else {
fmt.Println("通道已被关闭")
}
case data, ok := <-ch2:
if ok {
fmt.Println("从 ch2接收到数据: ", data)
} else {
fmt.Println("通道已被关闭")
}
}
}
select {
case data, ok := <-ch1:
if ok {
fmt.Println("从 ch1 接收到数据:", data)
} else {
fmt.Println("通道已被关闭")
}
case data, ok := <-ch2:
if ok {
fmt.Println("从 ch2接收到数据: ", data)
} else {
fmt.Println("通道已被关闭")
}
default:
fmt.Println("没有接收到数据,走 default 分支")
}
}
执行结果
从 ch1 接收到数据: 1
从 ch2接收到数据: 2
没有接收到数据,走 default 分支
上述示例中,首先创建了两个 channel
,ch1
和 ch2
,分别在不同的 goroutine
中向两个 channel
中写入数据。然后,在主 goroutine
中使用 select
语句监听两个channel
,一旦某个 channel
上有数据流动,就打印出相应的数据。由于 ch1
中的数据比 ch2
中的数据先到达,因此首先会打印出 "从 ch1 接收到数据: 1"
,然后才打印出 "从 ch2接收到数据: 2"
。
为了方便测试 default
分支,我写了两个 select
代码块,执行到第二个 select
代码块的时候,由于 ch1
和 ch2
都没有数据了,因此执行 default
分支,打印 "没有接收到数据,走 default 分支"
。
一些使用 select 与 channel 结合的场景
实现超时控制
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("接收到数据: ", data)
} else {
fmt.Println("通道已被关闭")
}
case <-time.After(2 * time.Second):
fmt.Println("超时了!")
}
}
执行结果为:超时了!
。
在这个例子中,程序将在 3 秒后向 ch
通道里写入数据,而我在 select
代码块里设置的超时时间为 2 秒,如果在 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("任务完成:", data)
} else {
fmt.Println("通道已被关闭")
}
}
}
}
执行结果(每次执行的顺序都会不一致):
任务完成: 1
任务完成: 5
任务完成: 2
任务完成: 3
任务完成: 4
任务完成: 0
任务完成: 9
任务完成: 6
任务完成: 7
任务完成: 8
在这个例子中,启动了 10 个 goroutine
并发执行任务,并使用一个 channel
来接收任务的完成情况。在主函数中,使用 select
语句监听这个 channel
,每当接收到一个完成的任务时,就进行处理。
监听多个通道的消息
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 开启 goroutine 1 用于向通道 ch1 发送数据
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
time.Sleep(time.Second)
}
}()
// 开启 goroutine 2 用于向通道 ch2 发送数据
go func() {
for i := 5; i < 10; i++ {
ch2 <- i
time.Sleep(time.Second)
}
}()
// 主 goroutine 从 ch1 和 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
多路复用,可以同时监听多个通道的数据,并避免了使用多个 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
语句只能用于通信操作,如channel
的读写,不能用于普通的计算或函数调用。select
语句会阻塞,直到至少有一个case
语句满足条件。 如果有多个case
语句满足条件,则会随机选择一个执行。如果没有
case
语句满足条件,并且有default
语句,则会执行default
语句。在
select
语句中使用channel
时,必须保证channel
是已经初始化的。如果一个通道被关闭,那么仍然可以从它中读取数据,直到它被清空,此时会返回通道元素类型的零值和一个布尔值,指示通道是否已关闭。
总之,在使用 select
语句时,要仔细考虑每个 case
语句的条件和执行顺序,避免死锁和其他问题。
来源:https://juejin.cn/post/7210005376653377597


猜你喜欢
- DATE_ADD(date,INTERVAL expr type) DATE_SUB(date,INTERVAL expr type)这些函
- 目录系列教程一、用户管理1、用户账号2、增加删除账号3、破解管理账号密码二、授权管理1、授权2、查询授权3、收回授权总结系列教程MySQL系
- 如果看到特别感兴趣的抖音vlogger的视频,想全部dump下来,如何操作呢?下面介绍介绍如何使用python导出特定用户所有视频信息抓包分
- 摘要什么是python对象的标识python对象相等的判断自定义python对象相等的条件python对象的标识python对象标识就是py
- 朴素贝叶斯估计朴素贝叶斯是基于贝叶斯定理与特征条件独立分布假设的分类方法。首先根据特征条件独立的假设学习输入/输出的联合概率分布,然后基于此
- 目录十大经典的排序算法 一、交换排序1、冒泡排序(前后比较-交换)2、快速排序(选取一个基准值,小数在左大数在右)二、插入排序1、
- 本文实例讲述了Go语言实现定时器的方法。分享给大家供大家参考。具体实现方法如下:package mainimport ( &quo
- hello,我是李华同学,最近开始学习爬虫,下面是我实现的一个得到弹幕的代码找一个的URL想要得到一个网站的内容,首先要找到你想要内容的具体
- 使用 designer 进行开发首先要知道,使用 Qt designer 和 代码进行 Qt 开发实现页面跳转是不一样的,这里我们使用的是
- goods表如下:name time productA 2016-1-2 13:23:
- 事务概念一个事务可以理解为一组操作,这一组操作要么全部执行,要么全部不执行。特性Read UncommitRead CommitRepeta
- 问题背景查询MySQL中用逗号分隔的字段【a,b,c】是否包含【a】场景模拟现有表【ec_logicplace】,如下图所示:要求判断数值【
- 使用echarts时created里拿到的数据无法渲染问题描述在vue里使用echart时,created里请求的数据,但是却无法渲染;代码
- 本文实例为大家分享了Python/C++实现字符串逆序的具体代码,供大家参考,具体内容如下题目描述:将字符串逆序输出Python实现一:借助
- 概述PHP有着众多的内置函数,其中大多数函数都被开发者广发使用。但也有一些同样有用却被遗忘在角落,本文将介绍7个鲜为人知功能却非常酷的函数。
- 身份证校验码的计算方法1、将前面的身份证号码17位数分别乘以不同的系数。第i位对应的数为[2^(18-i)]mod11。从第一位到第十七位的
- 例如:[‘a', ‘b', ‘c'] 输出 [‘a', ‘b', ‘c'] [‘a'
- 解决方法: 给 audio 组件绑定点击事件,手动触发播放暂停方法!代码片段:wxml文件<!-- 判断是语音通话,有通话记录,通话描
- 简述Motivationsometimes,换一种获取数据的方式,可以提高数据获取的速度。sometimes,由于预计爬取的数据长度不确定,
- 之前一直使用hdfs的命令进行hdfs操作,比如:hdfs dfs -ls /user/spark/hdfs dfs -get /user/