谈谈我对Golang 并发的理解
目录
什么是并发
并发是由多个独立执行的计算程序组成的,并发是一种结构化的软件程式。并发并不是并行,尽管他有并行的一些特性,如果只有一个处理器,程序仍然可以并发的,但是不能并行。
并发:同一时间段内执行多个任务 (时间段)
并行:同一时刻执行多个任务 (时刻)
编写一个随机时间内说hi的函数
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
sayHi("hi")
}
func sayHi(msg string) {
for i := 0; ; i++ {
fmt.Println(msg,i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}
输出:
hi 0
hi 1
hi 2
hi 3
...
goroutine
在main
中改造一下sayHi()
的方式,使用go
关键字
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
go sayHi("hi")
}
func sayHi(msg string) {
for i := 0; ; i++ {
fmt.Println(msg,i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}
以上代码执行之后,发现没有任何的输出。 go
语句会正常运行sayHi()
函数,但不会让主函数(调用方)等待。
他启动了一个goroutine,去执行sayHi()
,执行过程中main
函数并不会等待他执行完成。
我们让main
函数睡个2秒钟看看效果
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
go sayHi("hi")
fmt.Println("说的不错!")
time.Sleep(2*time.Second)
fmt.Println("不想听你说了,我走了。")
}
func sayHi(msg string) {
for i := 0; ; i++ {
fmt.Println(msg,i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}
以上程序执行效果如下
说的不错!
hi 0
hi 1
hi 2
hi 3
hi 4
hi 5
不想听你说了,我走了。
goroutine
是一个独立执行的函数,由go
关键字启动,他具有自己的调用堆栈,且堆栈会根据需要进行扩展何收缩,goroutine
不是线程(thread),一个程序中可能只有 一个线程,但是可以有很多个goroutine
。goroutine
会依据需要动态的多路复用到线程上,以保持所有的goroutine
运行。
在之前的例子中,我们看见终端打印goroutine
的输出,其实main
函数看不到goroutine
的输出,main
函数和goroutine
之间并没有数据的交互。
如果需要两者进行交互(沟通)需要使用channel
channel
go语言中使用通道来让两个goroutine链接,从而使它们之间能够进行通信。
channel 的声明和初始化
var c chan int
c = make(chan int)
// or
c := make(chan int)
//向channel中发送数据
c <- 1
//从channel中获取数据
value <- c
//箭头的指向代表了数据的流向
本篇文章不讨论有缓冲区的channel,仅讨论没有缓冲区的channel。
使用channel
改造下之前的函数
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
c := make(chan string)
go sayHi("hi", c)
for i := 0; i < 5; i++ {
fmt.Printf("你说了:%q\n", <-c)
}
fmt.Println("不想听你说了,我走了。")
}
func sayHi(msg string, c chan string) {
for i := 0; ; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}
运行结果:
你说了:"hi 0"
你说了:"hi 1"
你说了:"hi 2"
你说了:"hi 3"
你说了:"hi 4"
不想听你说了,我走了。
当main
函数执行到<-c
的时候,它会一直等待直到有数据可以读取出来。相反的,当sayHi()
执行c<-
时候,它会一直等待直到可以把数据传递进去(这里仅讨论没有缓冲区的通道)
发送者和接收者都必须准备好各自的状态,才能进行通信。否则就会一直等待他们两个都准备好为止(仅讨论无缓冲区的通道)
go语言中的channel,不通过共享内存来通信,而是通过通信共享内存。
改造下之前的代码,让sayHi()
返回一个channel,这样我们可以让更多人说不同的话
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
c1 := sayHi("hi")
c2 := sayHi("ha")
for i := 0; i < 5; i++ {
fmt.Printf("你说了:%q\n", <-c1)
fmt.Printf("你说了:%q\n", <-c2)
}
fmt.Println("不想听你说了,我走了。")
}
func sayHi(msg string) <-chan string {
c := make(chan string)
go func() {
for i := 0; ; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}()
return c
}
执行结果
你说了:"hi 0"
你说了:"ha 0"
你说了:"hi 1"
你说了:"ha 1"
你说了:"hi 2"
你说了:"ha 2"
你说了:"hi 3"
你说了:"ha 3"
你说了:"hi 4"
你说了:"ha 4"
不想听你说了,我走了。
以上代码在执行过程中会有一个问题,说的快的人必须等说的慢的人说好才能进行下一轮
使用多路复用(Multiplexing)
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
c := fanIn(sayHi("hi"), sayHi("ha"))
for i := 0; i < 20; i++ {
fmt.Println(<-c)
}
fmt.Println("不想听你说了,我走了。")
}
func fanIn(input1, input2 <-chan string) <-chan string {
c := make(chan string)
go func() {
for {
c <- <-input1
}
}()
go func() {
for {
c <- <-input2
}
}()
return c
}
func sayHi(msg string) <-chan string {
c := make(chan string)
go func() {
for i := 0; ; i++ {
c <- fmt.Sprintf("%s %d", msg, i)
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
}
}()
return c
}
以上程序多执行几次会发现其中一个会连续地输出,而不是等待另外一个准备好了在一起输出。
select
select
提供了处理多个通道的方法。类似于switch-case
语句结构
- 所有的通道都可以被执行
- 阻塞(block),直到其中一个channel可以通信,并执行他。
- 如果多个通道可以通信,则会伪随机的选择一个执行
- 如果没有可执行的case,在有default的情况下,会立即执行default
使用select改造fanIn()
函数达到同样的效果
func fanIn(input1, input2 <-chan string) <-chan string {
c := make(chan string)
go func() {
for {
select {
case s := <-input1:
c <- s
case s := <-input2:
c <- s
}
}
}()
return c
}
我们还可以使用time.After()
返回一个channel
达到定时退出效果
package main
func main() {
c := sayHi("hello")
timeout := time.After(3 *time.Second)
for{
select {
case <-timeout:
fmt.Println("timeout")
return
case v := <-c:
fmt.Println(v)
}
}
}
使用来channel实现传声游戏(部队中的报数,从队首依次报数+1至队尾)
package main
import "fmt"
//从右至左报数
func baoshu(left, right chan int) {
left <- 1 + <-right
}
func main() {
//假定有10000个人
const n = 10000
//先定义两个channel
leftmost := make(chan int)
right := leftmost
left := leftmost
//循环开始报数
for i := 0; i < n; i++ {
//初始化最右边的数
right = make(chan int)
//开始报数 左边的数等于右边+1
go baoshu(left, right)
//此时
left = right
}
go func(c chan int) {
c <- 1
}(right)
fmt.Println(<-leftmost)
}