GO 并发
简介
-
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
-
goroutine 是轻量级线程也有叫 用户级线程,协程的,
-
goroutine 的调度是由 Golang 运行时进行管理的。
-
你可理解为一段可以异步执行的代码,一个新的轻量级线程
-
进程 => 线程 =>协程
为什么会有协程的出现
每一个新鲜事物的出现,都是为了解决某一类问题的,那协程是为了解决什么?
这里用java的线程举例:
java的线程调度是经过内核的,也就是 Thread 和 kernel thread 是一比一的关系
也就是java 所谓的线程其实就是在内核的系统线程上包了一层java的东西,其中涉及到
用户态和内核态之间的切换,中间的过程非常复杂,非常耗时
有关用户态和内核态,可以参考:
https://www.cnblogs.com/zwj-199306231519/articles/16859489.html
这时候有人就开始想办法,想做出一个,
不经过内核,不涉及到用户态的切换,但又类似线程的东西,
这就是最开始协程的概念
goroutine 语法格式
go 函数名( 参数列表 )
例如:
go f(x, y, z)
开启一个新的 goroutine:
f(x, y, z)
Go 允许使用 go 语句开启一个新的运行期线程(协程), 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间
两个例子
例子1:异步
package main
import (
"fmt"
"time"
)
func GoRoutine() {
go func() {
time.Sleep(10 * time.Second)
}()
// 这里直接输出,不会等待十秒
fmt.Println("I am here")
}
func main() {
GoRoutine()
}
通过上面例子可以知道,go func
开启的方法是异步的
例子2:无序
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
通过输出可以知道,他们之间执行是无序的
通道(channel)简介
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
通道(channel) 声明,创建,赋值
声明一个通道很简单,我们使用chan关键字即可
var Channel_name chan Type
通道在使用前必须先创建:
ch := make(chan int)
赋值
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
例:
package main
import "fmt"
func main() {
//使用var关键字创建通道
var mychannel chan int
fmt.Println("channel的值: ", mychannel)
fmt.Printf("channel的类型: %T ", mychannel)
// 使用 make() 函数创建通道
mychannel1 := make(chan int)
fmt.Println("\nchannel1的值:", mychannel1)
fmt.Printf("channel1的类型: %T ", mychannel1)
}
通道(channel)的缓冲区
通道的种类分为:
- 带缓冲的
- 不带缓冲的
通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
channel 带缓冲
channel 不带缓冲
案例
package main
import (
"fmt"
"time"
)
func main() {
channelWithoutCache()
channelWithCache()
}
func channelWithCache() {
// 带缓冲,缓冲区为1
ch := make(chan string, 1)
go func() {
ch <- "Hello, first msg from channel"
time.Sleep(time.Second)
ch <- "Hello, second msg from channel"
}()
time.Sleep(2 * time.Second)
msg := <-ch
fmt.Println(time.Now().String() + msg)
msg = <-ch
fmt.Println(time.Now().String() + msg)
// 因为前面我们先睡了2秒,所以其实会有一个已经在缓冲了
// 当我们尝试输出的时候,这个输出间隔就会明显小于1秒
// 我电脑上的几次实验,差距都在1s以内
}
func channelWithoutCache() {
// 不带缓冲
ch := make(chan string)
go func() {
time.Sleep(time.Second)
ch <- "Hello, msg from channel"
}()
// 这里比较容易写成 msg <- ch,编译会报错
msg := <-ch
fmt.Println(msg)
}
通道的遍历与关闭
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭
例
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
// 将 x 的值写入通道
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}
通道的双向通道和单向通道
通道包括双向通道和单向通道,这里双向通道只的是支持发送和接收的通道,而单向通道是只能发送或者只能接收的通道。
双向通道
语法
使用make函数声明并初始化一个通道:
ch1 := make(chan string, 3)
chan
是表示通道类型的关键字string
表示该通道类型的元素类型3
表示该通道的容量为3,最多可以缓存3个元素值。
一个通道相当于一个先进先出(FIFO)的队列,使用操作符 <-
进行元素值的发送和接收:
ch1 <- "1" //向通道ch1发送数据 "1"
接收元素值:
elem1 := <- ch1 // 接收通道中的元素值
首先接收到的元素为先存入通道中的元素值,也就是先进先出
案例
package main
import "fmt"
func main() {
str1 := []string{"hello","world", "!"}
ch1 := make(chan string, len(str1))
for _, str := range str1 {
ch1 <- str
}
for i := 0; i < len(str1); i++ {
elem := <- ch1
fmt.Println(elem)
}
}
执行结果:
单向通道
语法
单向通道包括只能发送的通道和只能接收的通道:
// 写法1
var WriteChan = make(chan<- interface{}, 1) // 只能向通道发送不能接收的
var ReadChan = make(<-chan interface{}, 1) // 只能从通道中接收不能发送的
// 写法2
ch := make(chan int)
// 声明一个只能写入数据的通道类型, 并赋值为ch
var chSendOnly chan<- int = ch
//声明一个只能读取数据的通道类型, 并赋值为ch
var chRecvOnly <-chan int = ch
// 总结
// chan<- 只能向通道写入
// <-chan 只能从通道中读取
单向通道的这种特性可以用来约束函数的输入类型或者输出类型
官方案例-time包中的单向通道
ime 包中的计时器会返回一个 timer 实例,代码如下:
timer := time.NewTimer(time.Second)
timer的Timer类型定义如下:
type Timer struct {
C <-chan Time
r runtimeTimer
}
第 2 行中 C 通道的类型就是一种只能读取的单向通道。如果此处不进行通道方向约束,一旦外部向通道写入数据,将会造成其他使用到计时器的地方逻辑产生混乱。
因此,单向通道有利于代码接口的严谨性。
案例
下面这个例子,完整的展示了单向通道的流程
package main
import (
"fmt"
)
func main() {
// 默认为双向通道
chan1 := make(chan int)
chan2 := make(chan int)
//函数sendChan只允许发送数据,也就是写入到通道
go sendChan(chan1)
// 函数squarer将chan1的数据转给chan2
go squarer(chan2, chan1)
// 函数recvChan只允许接收数据,也就是从通道中读取
go recvChan(chan2)
// 阻塞main,循环随机监听通道
select {}
}
// (in <-chan int) 参数只允许发送数据,不允许接收
func sendChan(in chan<- int) {
i := 0
for {
in <- i
i++
}
}
// (out chan<- int) 参数只允许接收数据,不允许发送数据
func recvChan(out <-chan int) {
for i := range out {
fmt.Println("out输出:", i)
}
}
// (out chan<- int) 参数只允许接收数据,不允许发送数据
// (in <-chan int) 参数只允许发送数据,不允许接收
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i
}
close(out)
}
select 语句
elect 是 Go 中的一个控制结构,类似于 switch 语句。
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
语法
Go 编程语言中 select 语句的语法如下:
select {
case <- channel1:
// 执行的代码
case value := <- channel2:
// 执行的代码
case channel3 <- value:
// 执行的代码
// 你可以定义任意数量的 case
default:
// 所有通道都没有准备好,执行的代码
}
以下描述了 select 语句的语法:
-
每个 case 都必须是一个通道
-
所有 channel 表达式都会被求值
-
所有被发送的表达式都会被求值
-
如果任意某个通道可以进行,它就执行,其他被忽略。
-
如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
案例
不断地从两个通道中获取到的数据,当两个通道都没有可用的数据时,会输出 "no message received"。
package main
import "fmt"
func main() {
// 定义两个通道
ch1 := make(chan string)
ch2 := make(chan string)
// 启动两个 goroutine,分别从两个通道中获取数据
go func() {
for {
ch1 <- "from 1"
}
}()
go func() {
for {
ch2 <- "from 2"
}
}()
// 使用 select 语句非阻塞地从两个通道中获取数据
for {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
// 如果两个通道都没有可用的数据,则执行这里的语句
fmt.Println("no message received")
}
}
}