这是我在学习go语言的并发编程的过程中理解到的一些东西的分享
Goroutinue
go语言的协程,创建时为2kb或者4kb,所以成本是非常低的。Goroutine本身的语法非常非常简单。我们可以事先定义一个函数f() ,那么就可以通过go f()语句来开启一个协程。当然,闭包也是可以的。\
package main
func main() {
go func() {
for i := 0; i < 10; i++ {
println("hello")
}
}()
}
这里就开启了一个协程,开启的关键字就是go。根据我们的实际需求,也可以同时开启上千个协程。
但是如果你实际执行了这份代码,会发现没有任何的输出。这是因为go开启协程之后,main函数还会继续执行,而我在开启线程之后没有写别的代码,故程序直接就结束了运行。
那么,要怎么让程序能够输出东西呢?
package main
import "time"
func main() {
go func() {
for i := 0; i < 10; i++ {
println("hello")
}
}()
time.Sleep(10 * time.Millisecond)
}
我们可以让程序Sleep那么10ms,给协程时间完成这个过程。使用Sleep的方式来保证协程的完整执行,是可行的,但显然是不可靠的,也许我们的协程中并不是打印十次hello而是别的需要较长时间运行的功能呢?Sleep两个小时?但要是这个协程根据输入的不同可能是1微秒也可能是24个小时呢?这时候,就可以用上接下来介绍的功能了。
Channel
channel 是一个非常非常好用的东西,它可以作为一个灵活的工具来进行线程的操纵,而绝不是仅仅是作为一个线程间通信的载体那么简单。
channel有多种类型,但是常用的似乎就是第一种,既可以用来发送,也可以用来接收指定类型数据的channel
chan T // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据
可以使用chan := make(chan int, 100) 语句来声明一个被命名为chan,可以接收和发送int类型,大小为100的channel。
我们可以使用<-来操作channel\
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
ch <- 1
ret := <-ch
fmt.Println(ret)
}
上面的代码创建了一个大小为1的channel,首先往channel里放了一个整形数据1,然后又从中取出并赋值给了ret。
使用channel可以让我们刚刚写的协程程序运行起来更加优雅,不用再使用不太靠谱的sleep函数.
package main
func main() {
ch := make(chan int, 1)
go func() {
for i := 0; i < 10; i++ {
println("hello")
}
ch <- 1
}()
<-ch
}
新的代码结构非常简单,就是在之前的基础上写了一个channel来传输整形数据。启动go routine后,<- ch语句会让main函数保持阻塞状态,等待channel的结果。这样我们就可以有充足的时间用来执行打印十次"hello"的操作。而随着操作的完成,我们向管道中传入数据,就能结束阻塞状态,宣告程序的结束。
我们也可以用管道来维持多个协程的运行
package main
func main() {
ch := make(chan int, 1)
for i := 0; i < 3; i++ {
go func() {
println("hello")
ch <- 1
}()
}
for i := 0; i < 3; i++ {
<-ch
}
}
在这段代码中开启三个协程,他们各自打印一个hello,总共能够打印三个hello
Select
go语言提供了Select关键字用于读取channel中的信息并运行指定的代码块。使用起来会有类似很多编程语言中的switch语句的感觉,但是select与switch语句存在一个很明显的差异,那就是select会从各个情况中随机选取一个进行channel信息的读取和执行。而不是像switch语句一样从上往下遍历,看到哪一个满足条件就执行哪个。(在学校里写c/c++应该都会有忘记写break的经历吧)当然,select语句的随机选取的范围局限于有内容物的channel,没有东西的当然不会随机到
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
ch2 := make(chan int, 3)
ch3 := make(chan int, 3)
for i := 0; i < 3; i++ {
ch1 <- 1
ch2 <- 2
ch3 <- 3
}
for {
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2:
fmt.Println("ch2")
case <-ch3:
fmt.Println("ch3")
default:
}
}
}
该代码的运行结果,可以很清楚地看到,select的执行没有是随机的,没有确定的顺序。
值得注意的是,我在select语句的底部添加了一个空default,我写它并不是为了让他和switch看起来更像。实际上,这是为了防止这个程序报错。当select语句中的所有管道都被搬空了的时候,他就会执行default语句,即使default语句中没有任何代码,都可以让它反复地执行下去,直到再次在管道中得到信息。
一直让他这样执行的意义是什么呢?也许我们实际写的程序也会用到select来接收多个管道信息,但是如果程序暂时没有接到请求,管道都空了,程序就会报错崩溃。这绝对不是我们想要看到的,所以我们需要提供一个default字段,即使是空的也行,让程序能够等待请求的到来,在没人使用自己的时候也能耐得住寂寞😋