go语言中对多任务的执行基于并发模型,可以简单地理解为交替执行。不同于其他语言的多线程,go基于协程,即goroutine,是一种轻量级的线程。
对于数据的共享,go从语言层次提供了信道channels代替共享内存,当然共享内存go中也是支持的。
一、goroutine
goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。
1、每个子goroutine依赖于主goroutine,主goroutine不等待子goroutine
2、子goroutine被调用后忽略返回值。
3、goroutine非阻塞
01、go关键字
Go语言中的go关键字可以快速创建一个goroutine.
func main() {
fmt.Println("main...start")
go say()
fmt.Println("main....over")
}
func say(){
for i:=1;i<10;i++{
fmt.Println("hello",i)
}
}
main...start
main....over
主goroutine启动后开始执行,随后启动say的goroutine,不会等待say执行结束就会继续执行。main结束后,say作为依赖主goroutine的协程也会消亡。
02、sleep()
可以使用time包下的sleep函数让main睡眠一会儿,让say执行。
func main() {
fmt.Println("main...start")
go say()
time.Sleep(1*time.Second)
fmt.Println("main....over")
}
func say(){
for i:=1;i<10;i++{
fmt.Println("hello",i)
}
}
main...start
hello 1
hello 2
hello 3
hello 4
hello 5
hello 6
hello 7
hello 8
hello 9
main....over
03、并发执行
goroutine之间并发执行,没有绝对的顺序,所以满足一定约束的情况下,会有不同的结果。
func main() {
fmt.Println("main...start")//语句1
go say()
for i:=1;i<100;i++{
fmt.Println("ok",i)
}
fmt.Println("main....over")
}
func say(){
for i:=1;i<10;i++{
fmt.Println("hello",i)
}
}
必然是语句1先执行,然后goroutine之间互相竞争,以下是其中一个结果:
main...start
ok 1
。。
ok 21
hello 1
hello 2
hello 3
hello 4
ok 22
。。
ok 42
ok 43
hello 5
hello 6
hello 7
hello 8
hello 9
ok 44
。。。
main....over
二、信道channels
一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
01、特性
1、在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。
2、通道像队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
3、把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。
4、chan是引用类型
02、声明及创建
chan是有类型的,可以直接声明或者使用make关键字创造。
var c1 chan int//声明chan
var c2 chan map[int] string//map型的chan
c3:=make(chan *int)//int指针的chan
03、接收和发送
通道发送和接收同样使用<-
操作符号,箭头方向就是接收方。
var c1 chan int//声明chan
c1<-10 //c1接收数据10
data:=<-c1 //c1输出数据到data
fmt.Println(data)
以上的代码执行会是这样的结果:
fatal error: all goroutines are asleep - deadlock!
报错是因为只有一个goroutine,无法形成接收和发送方。在不同的goroutine之间才可以接收发送,我们在后面探讨。
接收和发送:
var c2 chan map[int] string//map型的chan
var m map[int]string= map[int]string{1:"hello",2:"chan",3:"!"}
c2<-m //接收map型的m
datas:=<-c2 //发送map型的m给datas
c3:=make(chan *int)//int指针的chan
n:=10
p:=&n
c3<-p //接收int指针
04、通道使用
001、普通并发---ch.go
首先看一个没有通道的例子:main和show是并发执行的
func main() {
fmt.Println("main ...start")
go show()
for i:=1;i<5;i++{
fmt.Println("...",i)
time.Sleep(1*time.Second)//每次执行睡眠一秒
}
fmt.Println("main...over...")
}
func show() {
fmt.Println("show函数")
}
main ...start
... 1
show函数
... 2
... 3
... 4
main...over...
002、通道错误----ch2.go
接下来我们加入通道:
func main() {
ch:=make(chan int)
fmt.Println("main ...start")
go show()
for i:=1;i<5;i++{
fmt.Println("...",i)
time.Sleep(1*time.Second)//每次执行睡眠一秒
}
ch<-10
fmt.Println("main...over...")
}
func show() {
fmt.Println("show函数")
}
执行会报错,因为在main像通道发送数据时,show已经结束了。通道数据没有接收方。
main ...start
... 1
show函数
... 2
... 3
... 4
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
003、通道错误---ch3.go
在show启动前就向信道发送数据:
func main() {
ch:=make(chan int)
fmt.Println("main ...start")
ch<-10
go show()
for i:=1;i<5;i++{
fmt.Println("...",i)
time.Sleep(1*time.Second)//每次执行睡眠一秒
}
fmt.Println("main...over...")
}
func show() {
fmt.Println("show函数")
}
同理,信道接收数据后发现没有子goroutine的存在,直接报错。
main ...start
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
004、通道阻塞---ch4.go
在main的循环中让信道接收数据,在子goroutine中没有接收数据
func main() {
fmt.Println("main ...start")
go show()
ch:=make(chan int)
for i:=1;i<100;i++{
fmt.Println("...",i)
ch<-10
}
fmt.Println("main...over...")
}
func show() {
for i:=1;i<100;i++{
fmt.Println("show",i)
}
}
结果如下,信道中的数据没有被接收,那么该goroutine将会被阻塞,直到子goroutine结束。
main ...start
... 1
show 1
show 2
show 3
show 4
。。。。
show 99
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
005、通道正常---ch5.go
正常的通道是有发送方和接收方,并且两者都是存活的。
func main() {
fmt.Println("main ...start")
ch:=make(chan int)
go show(ch)
for i:=1;i<100;i++{
fmt.Println("...",i)
ch<-i//进入通道
}
fmt.Println("main...over...")
}
func show(c chan int) {
for i:=1;i<100;i++{
fmt.Println("show",i)
data:=<-c//出通道,先进先出
fmt.Println("通道数据",data)
}
}
结果如下:通道数据是队列形式,有进必须有出,否则就会阻塞。接收方接收通道里面的数据时,如果通道无数据,也必须等待数据进入通道,否则也会阻塞。
main ...start
... 1
show 1
通道数据 1
show 2
... 2
... 3
通道数据 2
show 3
通道数据 3
show 4
... 4
... 5
通道数据 4
show 5
通道数据 5
show 6
... 6
... 7
通道数据 6
show 7
通道数据 7
show 8
... 8
... 9
通道数据 8
。。。。
show 98
... 98
... 99
通道数据 98
show 99
通道数据 99
main...over...
006、关闭通道--close()
通道被使用完后和其他引用类型一样也是会被回收的,不过我们也可以主动关闭。close()函数提供了这样一个功能,并且关闭的通道将不会阻塞。
1)主函数中接收不到数据就会阻塞,从而引发异常。
func main() {
fmt.Println("main...start")
ch:=make(chan int)
go say(ch)
fmt.Println(<-ch)//无数据可被读出
fmt.Println("main....over")
}
func say( ch chan int){
fmt.Println("发送数据。。。")
}
fatal error: all goroutines are asleep - deadlock!
main...start
发送数据。。。
goroutine 1 [chan receive]:
main.main()
。。。。
2)使用close()关闭通道,则不会堵塞,此时其他groutine取到通道里的默认值。
func main() {
fmt.Println("main...start")
ch:=make(chan int)
go say(ch)
data,ok:=<-ch//默认值为0
fmt.Printf("data: %v, ok: %v \n ",data,ok)
fmt.Println("main....over")
}
func say( ch chan int){
close(ch)
fmt.Println("发送数据。。。")
}
此时main不会堵塞。
main...start
发送数据。。。
data: 0, ok: false
main....over
3)这里的双返回值中的ok,如果通道没有关闭,取到的是true,通道已经关闭,取到的是false,并且此时data为通道类型的默认值。
data,ok:=<-ch
05、缓冲通道
之前说过通道内部逻辑结构是队列———先进先出,但是一般的通道只能存取一个数据(包括数组、map等),其实还有一种带缓冲的通道,能够存取多个数据。
001、特性
1、带缓冲的通道能够存入多个数据。
2、带缓冲的通道容量未满时,不会阻塞。
002、创建
创建也很简单,和普通通道一样,只不过带了缓冲大小。
通道实例 := make(chan 通道类型, 缓冲大小)
注意:原本通道是可以存取1个数据的,缓冲为n的通道可以存取n+1个数据。
003、使用
func main() {
fmt.Println("main...start")
ch:=make(chan int,3)//缓冲大小为3
go say(ch)
for {
data,ok:=<-ch //语句1
fmt.Printf("data: %v, ok: %v \n ",data,ok)//语句2
if !ok{
break //通道关闭则跳出循环
}
}
fmt.Println("main....over")
}
func say( ch chan int){
for i:=1;i<20;i++{
ch<-i //语句3
fmt.Println("写入数据",i)//语句4
}
close(ch)//关闭通道
fmt.Println("发送结束。。。")//语句5
}
可能会得到以下结果:
main...start
写入数据 1 //语句1无数据可取出,堵塞,语句3执行,写入四个数据
写入数据 2
写入数据 3
写入数据 4
data: 1, ok: true //语句1开始取数据,取出n个数据后,语句3又写入1个数据,语句1继续取数据
data: 2, ok: true
data: 3, ok: true
data: 4, ok: true
data: 5, ok: true
写入数据 5 //数据被写入后语句4还未执行,数据被取出才执行
写入数据 6
写入数据 7
写入数据 8
写入数据 9
data: 6, ok: true
data: 7, ok: true
data: 8, ok: true
data: 9, ok: true
data: 10, ok: true
写入数据 10
写入数据 11
写入数据 12
写入数据 13
写入数据 14
data: 11, ok: true
data: 12, ok: true
data: 13, ok: true
data: 14, ok: true
data: 15, ok: true
写入数据 15
写入数据 16
写入数据 17
写入数据 18
写入数据 19
发送结束。。。
data: 16, ok: true
data: 17, ok: true
data: 18, ok: true
data: 19, ok: true
data: 0, ok: false //关闭通道
main....over
06、定向通道
go语言提供了单向通道,也就是只读和只写的通道(只能用于发送或者接收数据),这是对通道的一种限制。
通常作为参数出现,限制在函数中的行为来保证数据安全,否则单纯的创建这种通道基本没有意义。
001、创建
单向 channel 变量的声明非常简单,只能发送的通道类型为chan<-
,只能接收的通道类型为<-chan
,格式如下:
var 通道实例 chan<- 元素类型 // 只能发送通道
var 通道实例 <-chan 元素类型 // 只能接收通道
使用make函数创建如下:
c1:=make(chan <-int)//只写
c2:=make(<-chan int)//只读
002、使用
我们在创建通道通常都是双向的,否则无法使用,作为参数时才会作为单向通道来限制数据读写。
如语句1创建通道,语句2设置为只写通道。
func main() {
fmt.Println("main...start")
ch:=make(chan int,3)//语句1
go say(ch)
for {
data,ok:=<-ch
fmt.Printf("data: %v, ok: %v \n ",data,ok)
if !ok{
break
}
}
fmt.Println("main....over")
}
func say( ch chan <-int){//语句2
for i:=1;i<20;i++{
ch<-i
fmt.Println("写入数据",i)
}
close(ch)
fmt.Println("发送结束。。。")
}
此时在say函数中只允许写入数据,不允许取出数据。
07、select
go中提供了select语句对通道进行复用,用来同时响应多个通道的操作。
select 的用法与 switch 语句非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。
001、声明
类似switch语句,其中每个case对应一个对通道的操作。
select{
case 操作1:
响应操作1
case 操作2:
响应操作2
…
default:
没有操作情况
}
002、特性
1、case语句必须包含对通道的操作,如读取、写入。
2、如果某个case可以被执行,就会公平的从中选一个执行,其他case和default被忽略。
3、没有case可以被执行,执行default.
go语言在语言层次提供了通道支持并发,也提供了传统的加锁机制来共享内存支持并发。