目录
1 并发
1.1 简介
Go 语言支持并发,通过 goroutines
和 channels
提供了一种简洁且高效的方式来实现并发。
1.2 Goroutine
1.2.1 简介
goroutine
协程,是轻量级线程,goroutine
的调度是由 Golang
运行时进行管理的,Go 中的并发执行单位,类似于轻量级线程
。
Goroutine
的调度由 Go 运行时
管理,用户无需手动分配线程,使用 go
关键字启动 Goroutine
,以一个不同的、新创建的 goroutine
来执行一个函数。 同一个程序中的所有 goroutine
共享同一个地址空间。
Goroutine
是非阻塞的
,可以高效地运行成千上万个 Goroutine
。
goroutine 语法格式:go 函数名( 参数列表 )
例如:go f(x, y, z)
注意
:
- 当一个新的
Go协程
启动时,协程的调用立即返回。与函数不同,程序流程不会等待Go协程
结束再继续执行。程序流程在开启Go协程
后立即返回并开始执行下一行代码,并忽略Go协程的任何返回值。 - 在主协程存在时才能运行其他协程,
主协程终止则程序终止,其他协程也将终止
1.2.2 特点
goroutine特点:
- 可增长的栈
OS
线程(操作系统线程)一般都有固定的栈内存(2MB)
,一个goroutine
的栈在生命周期开始时只有很小的栈(2KB),goroutine
的栈是不固定的,可以按需增加或者缩小,goroutine
的栈大小限制可以达到1GB,虽然这种情况不多见,所以一次可以创建十万左右的goroutine是没问题的。 - goroutine 调度
OS
线程由OS
内核来调度,goroutine
则是由Go
运行时(runtime
)自己的调度器来调度,这个调度器使用一个m:n
调度的技术(复用/调度m个goroutine
到n个OS线程
),goroutine
的调度不需要切换内核语境
,所以调用一个goroutine比调用个线程的成本要低很多。 - GOMAXPROCS
Go
运行时的调度使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码,默认值是机器上的CPU
核心数。
例如:在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS
是m:n
调度中的n
)。
Go
可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。(Go1.5版本前默认是单核心执行,Go1.5版本后默认使用全部逻辑核心数)
1.2.3 检测数据访问冲突
使用 go run -race
检测数据访问冲突
比如检测 某一个内存被写的时候刚好也被另一个协程读
o run -race goroutine.go
==================
WARNING: DATA RACE
Read at 0x00c000138000 by main goroutine:
main.main()
/Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:18 +0xfb
Previous write at 0x00c000138000 by goroutine 7:
main.main.func1()
/Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:13 +0x68
Goroutine 7 (running) created at:
main.main()
/Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:11 +0xc3
==================
[11755340 5733960 10421739 12104071 7264193 14611763 1966171 6718099 11141482 2270786]
Found 1 data race(s)
exit status 66
1.2.4 示例
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello")
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 使用 1 个逻辑核心数跑 Go 程序
runtime.GOMAXPROCS(1)
go sayHello() // 启动 Goroutine
for i := 0; i < 5; i++ {
fmt.Println("Main")
time.Sleep(100 * time.Millisecond)
}
}
1.3 通道(Channel)
1.3.1 普通通道
1.3.1.1 简介
通道(Channel
)是用于 Goroutine
之间的数据传递
,通道可用于两个 goroutine
之间通过传递一个指定类型的值来同步运行
和通讯
,避免了显式的锁机制。
go语言的并发模型是SCP
,提倡通过通信共享内存而不是通过共享内存而实现通信
通道(channel
)是引用类型
,是一种特殊的类型通道像一个传送带或者队列,遵循先进先出
原则,类似于队列,保证收发数据的顺序每一个通道都是一个具体类型的导管,在声明channel
的时候需要为其指定元素类型
注意
:对信道发送和接收数据默认是阻塞的
当数据发送到信道时,程序在发送语句处阻塞,直到其他协程从该信道中读取数据。类似地,当从信道读取数据时,程序在读取语句处被阻塞,直到其他协程向信道写入数据。
信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的。
1.3.1.2 声明通道
声明一个通道很简单,我们使用chan
关键字即可,通道在使用前必须先创建:ch := make(chan int)
- 如果使用
channel
之前没有make
,会出现deadlock
错误
声明一个channel
类型的变量时,实际上它是一个nil
值。未初始化的channel
是nil
,无法进行发送或接收操作。 - 写数据之前,没有其他协程阻塞接收并且没有缓冲可以存,会发生 dealock
- channel make 之后没有传入数据,提取数据的时候会 deadlock
- channel 满了继续传入元素会 deadlock
使用 make
函数创建一个 channel
,使用 chan
关键字表示 channel
,通过 <-
操作符发送
和接收
数据,如果未指定方向,则为双向通道(即:可读可写)
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据 并把值赋给 v
go func(c chan int) { channel c } (a)//双向通道,读写均可的
go func(c <- chan int) { } (a) //只读的Channel
go func(c chan <- int) { } (a)//只写的Channel
- 向
channel
传入数据,CHAN <- DATA
,CHAN
指的是目的 channel
即收集数据的一方,DATA
则是要传的数据。 - 从
channel
读取数据,DATA := <-CHAN
,和向channel
传入数据相反,在数据输送箭头的右侧的是channel
,形象地展现了数据从隧道流出到变量里。
注意
:默认情况下,通道是不带缓冲区
的。发送端发送数据,同时必须有接收端相应的接收数据。
1.3.1.3 普通通道示例
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
输出结果为:
-5 17 12
1.3.2 带缓冲区通道
1.3.2.1 简介
通道可以设置缓冲区
,通过 make
的第二个参数指定缓冲区大小:ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送
和接收端
的数据获取处于异步
状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据,还是遵循先进先出
原则
不过由于缓冲区的大小是有限的,所以还是必须有接收端
来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意
:如果通道不带缓冲
,发送方会阻塞
直到接收方从通道中接收了值。如果通道带缓冲
,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
读取操作
: 当读取一个通道时,只有通道中有值时,读取才会成功,否则会阻塞。写入操作
: 往通道写入值时,只有通道能够接收(缓冲区未满或有接收方)时,写入才会成功,否则会阻塞。
1.3.2.2 带缓冲区通道示例
package main
import "fmt"
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
执行输出结果为:
1
2
1.3.3 遍历
1.3.3.1 for 遍历
使用 v, ok := <-ch
格式,如果通道接收不到数据后 ok 就为 false
,相当于一直循环着等待有结果输出
package main
import "fmt"
var ch1 chan int
func send(ch chan int) {
for i:=0;i<10;i++ {
ch <- i
}
close(ch)
}
func main() {
ch1 = make(chan int,100)
go send(ch1)
// 从通道中取值
for {
// 产生两个值,获取的值给 ret,是否取完的 bool 值给 ok
ret,ok := <-ch1
// 判断值是否取完
if !ok {
// !ok 等于 !ok == true
break
}
fmt.Println(ret)
}
}
1.3.3.2 range 遍历与关闭通道
通过 range
关键字来实现遍历读取到的数据,类似于与数组或切片。
关闭通道
并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
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)
}
}
1.3.3.3 Select
select
是 Go 中的一个控制结构,类似于 switch
语句。但是,select
语句只能用于通道操作,每个 case
必须是一个通道操作,要么是发送
要么是接收
。
select
语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select
语句会随机选择
一个通道执行,其他不会执行
。如果所有通道都没有准备好,那么执行 default
块中的代码。
如果没有 default
子句,select
将阻塞,直到某个通道可以运行;Go
不会重新对 channel
或值进行求值。
select
语句使得一个 goroutine
可以等待多个通信操作。select
会阻塞,直到其中的某个 case
可以继续执行
Go 编程语言中 select
语句的语法如下:
select {
case <- channel1:
// 执行的代码
case value := <- channel2:
// 执行的代码
case channel3 <- value:
// 执行的代码
// 可以定义任意数量的 case
default:
// 所有通道都没有准备好,执行的代码
}
示例一:
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
结果为:
received one
received two
示例二:
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
示例三:
package main
import "fmt"
func main() {
var ch = make(chan int,1)
for i:=0;i<10;i++ {
// 解决死锁问题
select {
case ch <- i: //尝试放入值,只能放入一个值,如果有值,则无法再放入
case ret := <-ch: //尝试取值,没有值拿就会出现死锁
fmt.Println(ret)
}
}
}
/*
0
2
4
6
8
*/
1.3.3.4 空select
package main
func main() {
select {
}
}
我们知道select
语句将会被阻塞直到其中一个case分支可执行。这个例子,select
语句没有任何case
分支,因此它将被永久阻塞导致死锁。程序将会触发 panic,输出如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/main.go:4 +0x20
标签:1.3,goroutine,select,Go,main,Channel,通道
From: https://www.cnblogs.com/jingzh/p/18638385