Go 并发
Go 语言支持并发,只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
go 函数名( 参数列表 )
例如:
go f(x, y, z)
开启一个新的 goroutine:
f(x, y, z)
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
实例
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")
}
执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行:
world hello hello world world hello hello world world hello
通道(channel)
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch v := <-ch // 从 ch 接收数据 // 并把值赋给 v
声明一个通道很简单,使用chan关键字即可,通道在使用前必须先创建:
ch := make(chan int)
注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须又接收端相应的接收数据。
以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:
实例
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
通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
实例
import "fmt"
func main() {
// 这里定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
执行输出结果为:
1 2
Go 遍历通道与关闭通道
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
实例
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)
}
}
执行输出结果为:
0 1 1 2 3 5 8 13 21 34
大王叫我来巡山1998
Go语言从语言层面上 支持 并发
Go语言 goroutines 信道 和 死锁
Go语言 有个 goroutine 概念 类似 线程 但是更轻
串行 执行两次loop函数
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d " i)
}
}
func main() {
loop()
loop()
}
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
把 loop 放在 goroutine 里跑 使用 go 定义 启动一个goroutine
func main() {
go loop() // 启动一个goroutine
loop()
}
//0 1 2 3 4 5 6 7 8 9
明明 主线跑了一趟 goroutine 为什么只输出了一趟呢?
原来 在 goroutine 没来得及跑loop 主函数已经退出了
我们要想办法阻止 main函数 过早地退出
func main() {
go loop()
loop()
time.Sleep(time.Second) // 停顿一秒 让main等待一下
}
目的达到了 可办法并不好
如果goroutine 结束 时 告诉下主线说“Hey 我跑完了!” 就好了 即所谓阻塞主线
信道是什么?
简单说 是 goroutine 间互相通讯的东西 类似 Unix 管道(可以在进程间传递消息) 用来 goroutine 间发/接收消息 就是在做 goroutine 间的内存共享
使用make来建立一个信道
var channel chan int = make(chan int)// 或
channel := make(chan int)//just oney in func ,otherwise non-declaration statement outside function body
如何向 信道存/取消息呢
func main() {
messages := make(chan string)
go func(message string) {
messages <- message // 存消息
}("Ping!")
fmt.Println(<-messages) // 取消息 Ping!
}
默认的 信道的存消息和取消息都是阻塞的 (叫 无缓冲 信道 )
无缓冲 信道在取消息和存消息的时候 会挂起当前 goroutine 除非另一端已经准备好
main函数和 foo 函数
var ch chan int = make(chan int)
func foo() {
ch <- 0 // 向 ch 加数据 如果没有其他goroutine来取走这个数据 那么挂起foo 直到main函数把0这个数据拿走
}
func main() {
go foo()
<- ch // 从 ch 取数据 如果ch中还没放数据 那就挂起 main线 直到 foo 函数中放数据为止
}
既然信道可以阻塞当前 goroutine 如何让goroutine告诉主线 执行完毕了 使用一个信道来告诉主线即可
var complete chan int = make(chan int)
///complete := make(chan int)//non-declaration statement outside function body
func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
complete <- 0 // 执行完毕了 发个消息
}
func main() {
go loop()
<- complete // main在此阻塞住 // 直到线程跑完 取到消息
fmt.Printf("ok" )
}
如果不用信道来阻塞主线的话 主线就会过早跑完 loop线都没有机会执行
无缓冲的信道永远不会存储数据 只负责数据的流通 为什么这么讲呢?
从无缓冲信道取数据 必须要有数据流进来才可以 否则当前线阻塞
数据流入无缓冲信道 如果没有其他goroutine来拿走这个数据 那么当前线阻塞
所以 可以测试下 无论如何 测试到的无缓冲信道的大小都是0 (len(channel))
如果信道正有数据在流动 还要加入数据 或者信道干涩 一直向无数据流入的空信道取数据 就会引起死锁
死锁
func main() {
ch := make(chan int)
<- ch // 阻塞main goroutine 信道c被锁
}
执行这个程序 报这样的错误 fatal error: all goroutines are asleep - deadlock!
何谓死锁
所有的线程 或 进程都在等待资源的释放 只有一个 goroutine 所以当 向里面加数据或者存数据 都会锁死信道 并且阻塞当前 goroutine
也就是所有的 goroutine(其实就main线一个) 都在等待信道的开放(没人拿走数据信道是不会开放的) 也就是死锁咯
只在单一的 goroutine 里操作无缓冲信道 一定死锁 比如 只在main函数里操作信道
func main() {
ch := make(chan int)
ch <- 1 // 1流入信道 堵塞当前线 没人取走数据信道不会打开
fmt.Println("This line code wont run") //在此行执行之前Go就会报死锁
}
如下也是一个死锁的例子
var ch1 chan int = make(chan int)
var ch2 chan int = make(chan int)
func say(s string) {
fmt.Println(s)
ch1 <- <- ch2 // ch1 等待 ch2流出的数据
}
func main() {
go say("hello")
<- ch1 // 堵塞主线
}
其中主线等 ch1 中的数据流出 ch1等ch2的数据流出 但是ch2等待数据流入 两个goroutine都在等 也就是死锁
为什么会死锁?
这样理解 Go启动的所有goroutine里的 非缓冲信道 一定要
一个线里存数据
一个线里取数据
要成对才行
所以下面的示例一定死锁:
c, quit := make(chan int), make(chan int)
go func() {
c <- 1 // c通道的数据没有被其他goroutine读取走 堵塞当前goroutine
quit <- 0 // quit始终没有办法写入数据
}()
<- quit // quit 等待数据的写
是由于 主线等待quit信道的数据流出 quit等待数据写入 而func被c通道堵塞 所有goroutine都在等 所以死锁
简单来看的话 一共两个线 func线中流入c通道的数据并没有在main线中流出 肯定死锁
但是 是否果真 所有不成对向信道存取数据的情况都是死锁?
func main() {
c := make(chan int)
go func() {
c <- 1
}()
}
程序正常退出了 很简单 并不是我们那个总结不起作用了 还是因为一个让人很囧的原因 main又没等待其它goroutine 自己先跑完了
所以没有数据流入c信道 一共执行了一个goroutine 并且没有发生阻塞 所以没有死锁错误
那么死锁的解决办法呢?
最简单的 把没取走的数据取走 没放入的数据放入 因为无缓冲信道不能承载数据 那么就赶紧拿走!
具体来讲 就死锁例子3中的情况 可以这么避免死锁:
c quit := make(chan int) make(chan int)
go func() {
c <- 1
quit <- 0
}()
<- c // 取走c的数据!
<-quit
另一个解决办法是缓冲信道 即设置c有一个数据的缓冲大小:
c := make(chan int, 1)
这样的话 c可以缓存一个数据 也就是说 放入一个数据 c并不会挂起当前线 再放一个才会挂起当前线直到第一个数据被其他goroutine取走 也就是只阻塞在容量一定的时候 不达容量不阻塞
无缓冲信道的数据进出顺序
我们已经知道 无缓冲信道从不存储数据 流入的数据必须要流出才可以
观察以下的程序
var ch chan int = make(chan int)
func foo(id int) { //id: 这个routine的标号
ch <- id
}
func main() {
// 开启5个routine
for i := 0; i < 5; i++ {
go foo(i)
}
// 取出信道中的数据
for i := 0; i < 5; i++ {
fmt.Print(<- ch)
}
}
开了5个goroutine 然后又依次取数据 其实整个的执行过程细分的话 5个线的数据
依次流过信道ch main打印之 而宏观上我们看到的即 无缓冲信道的数据是 先到先出 但是 无缓冲信道并不存储数据 只负责数据的流通
缓冲信道
缓存信道 buffered channel
缓冲信道 不仅可以流通数据 还可以缓存数据 是有容量的 存入一个数据的话 可以先放在信道里 不必阻塞当前线而等待该数据取走
当缓冲信道达到满的状态的时候 就会表现出阻塞了 因为这时再也不能承载更多的数据了 「你们必须把 数据拿走 才可以流入数据」
在声明一个信道的时候 给make以第二个参数来指明它的容量(默认为0 即无缓冲)
var ch chan int = make(chan int, 2)// 写入2个元素都不会阻塞当前goroutine 存储个数达到2的时候会阻塞
如下的例子 缓冲信道ch可以无缓冲的流入3个元素
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
}
如果 再试图流入一个数据的话 信道ch会阻塞main线 报死锁
也就是说 缓冲信道会在满容量的时候加锁
其实 缓冲信道是先进先出的 我们可以把缓冲信道看作为一个线程安全的队列:
func main() {
ch := make(chan int 3)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
}
信道数据读取和信道关闭
上面的代码一个一个地去读取信道简直太费事了 Go语言允许 使用range来读取信道
func main() {
ch := make(chan int,3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
}
}//fatal error: all goroutines are asleep - deadlock!
报死锁错误的 原因是 range 不等到信道关闭 是不会结束读取的 也就是如果 缓冲信道干涸了 那么range就会阻塞当前 goroutine 所以死锁咯
试着避免这种情况 比较容易想到的是读到信道为空的时候就结束读取
func main() {
ch := make(chan int,3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Print(v,",")
if len(ch) <= 0 { // 如果现有数据量为0 跳出循环
break
}
}
}//1,2,3,
注意检查信道大小的方法 不能在信道 存 取 都在发生的时候 用于取出所有数据 这个例子 是因为我们只在ch中存了数据 现在一个一个往外取 信道大小是递减的
另一个方式是显式地关闭信道
ch := make(chan int,3)
ch <- 1
ch <- 2
ch <- 3
close(ch)// 显式地关闭信道
for v := range ch {
fmt.Print(v,",")
}//1,2,3,
被关闭的信道会禁止数据流入 只读的 仍然可以从关闭的信道中取出数据 但是不能 写入数据了
等待多gorountine的方案
使用 信道堵塞主线 等待开出去的所有 goroutine 跑完
开出很多小goroutine 各自跑各自的 最后跑完了向主线报告
如下2个版本的方案
只使用单个无缓冲信道阻塞主线
使用容量为goroutines数量的缓冲信道
对于方案1 代码
var quit chan int // 只开一个信道
func foo(id int) {
fmt.Print (id,",")
quit <- 0 // ok finished
}
func main() {
count := 10
quit = make(chan int) // 无缓冲
// quit = make(chan int, count) // // 容量10
for i := 0; i < count; i++ {
go foo(i)
}
for i := 0; i < count; i++ {
<- quit
}
}//0,1,2,3,4,5,6,7,8,9,
对于方案2
把信道换成缓冲10的
quit = make(chan int, count) // // 容量10
区别仅仅在于一个是缓冲的 一个是非缓冲的
对于这个场景而言 两者都能完成任务 都是可以的
无缓冲的信道是一批数据一个一个的「流进流出」
缓冲信道则是一个一个存储 然后一起流出去
文人墨客
形象说明一下无缓冲和有缓冲的区别:
无缓冲是同步的,例如 make(chan int),就是一个送信人去你家门口送信,你不在家他不走,你一定要接下信,他才会走,无缓冲保证信能到你手上。
有缓冲是异步的,例如 make(chan int, 1),就是一个送信人去你家仍到你家的信箱,转身就走,除非你的信箱满了,他必须等信箱空下来,有缓冲的保证信能进你家的邮箱。
修改一下上面笔记中的程序如下:
结果:
修改成 make(chan int, 2),同时合并:
为:
可以看到 after channel pro 没有被阻塞了。
结果:
文人墨客
Channel 是可以控制读写权限的 具体如下:
文人墨客
关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入
文人墨客
更好的展示边入边出概念:
文人墨客
我们单独写一个 say2 函数来跑 goroutine,并且 Sleep 时间设置长一点,150 毫秒,看看会发生什么:
输出结果:
问题来了,say2 只执行了 3 次,而不是设想的 5 次,为什么呢?
原来,在 goroutine 还没来得及跑完 5 次的时候,主函数已经退出了。
我们要想办法阻止主函数的结束,要等待 goroutine 执行完成之后,再退出主函数:
我们引入一个信道,默认的,信道的存消息和取消息都是阻塞的,在 goroutine 中执行完成后给信道一个值 0,则主函数会一直等待信道中的值,一旦信道有值,主函数才会结束。
文人墨客
goroutine 是 golang 中在语言级别实现的轻量级线程,仅仅利用 go 就能立刻起一个新线程。多线程会引入线程之间的同步问题,在 golang 中可以使用 channel 作为同步的工具。
通过 channel 可以实现两个 goroutine 之间的通信。
创建一个 channel, make(chan TYPE {, NUM}) TYPE 指的是 channel 中传输的数据类型,第二个参数是可选的,指的是 channel 的容量大小。
向 channel 传入数据, CHAN <- DATA , CHAN 指的是目的 channel 即收集数据的一方, DATA 则是要传的数据。
从 channel 读取数据, DATA := <-CHAN ,和向 channel 传入数据相反,在数据输送箭头的右侧的是 channel,形象地展现了数据从隧道流出到变量里。