goroutine
不同于其他语言的线程,一个Go语言程序中并发执行体是goroutine
默认一个程序只有一个goroutine,其负责调用main函数执行程序
编程时,可以认为goroutine是线程(虽然完全不是),进而写出正确的程序
goroutine创建
像defer一样: go function()
go ins.method()
用于创建goroutine,goroutine将执行go
关键字给定的函数、方法
go func()
不是等待func执行完成后才返回,而是立即返回,之后goroutine会异步执行,像线程一样
goroutine结束
当goroutine执行完其函数、方法后,将自动退出(像线程)
此外,当程序从main函数中返回后,此程序的所有goroutine都被强行终止(像线程)
我们没有直接的办法可以杀死一个goroutine,而只可以使用通信方式来关闭之
通道
goroutine是Go程序的并发执行体,而通道(channel)则是goroutine之间的消息通道,是goroutine们之间互相通信的消息队列
通道的创建
与map类似,通道变量是实际通道的引用,其类型是chan T
,T是通道内元素的类型
通道变量的默认值是nil,即
var ch chan int
ch == nil //true
make创建通道且指定通道容量,并由通道变量对其引用:
默认通道容量是0
ch := make(chan int)
//ch是chan int类型,0容量通道,是无缓冲通道
ch := make(chan int, 0)
//ch是chan int类型,0容量通道,是无缓冲通道
ch := make(chan int, 3)
//ch是chan int类型,3容量通道,是缓冲通道
由于通道变量是引用类型,故函数参数传值相当于传引用,是没毛病的
通道操作1、发送数据
ch <- x //将元素x发送到通道ch
通道操作2、接收数据
x := <-ch //将通道中元素弹出赋与变量x
<-ch //将通道中元素弹出并丢弃
通道操作3、关闭通道
close(ch)
- 关闭通道后,在此通道再发送数据会导致宕机
- 一般当发送者发完消息后,才关闭通道
- 关闭通道后,在此通道上无论再接收多少次数据,都将立刻得到通道元素类型
T
的零值
注意:
通道ch关闭后,变量ch并不被自动垃圾回收,与文件描述符的close不是一回事
通道变量的回收与普通变量的回收别无区别,没人可引才回收
无缓冲通道(cap=0)
无缓冲通道上进行:
- 写操作:将会阻塞,直到接收方对管道执行接收操作
- 读操作:将会阻塞,直到发送发对管道执行发送操作
于是,无缓冲通道使得发送方、接收方完全同步化,故也叫“同步通道”
缓冲通道
缓冲通道上进行:
- 写操作:将数据加到管道尾端,管道若已满则阻塞
- 读操作:从管道头部弹出数据,管道若已空则阻塞
缓冲通道使得发送者、接收者相对变得解耦
接收者判断通道关闭
由于close将导致管道永远给接收者返回零值,则: chan int
管道关闭后,接收者将永远收到0
但是我们无法因为收到0就认为管道关闭,因为有可能发送者确实发送了0
那么接收者如何判断管道关闭?
方法1:
x, ok := <-ch
ok值为true
表示管道有数据;否则表示管道已关闭
方法2:
for x:= range ch {
...
}
//当通道关闭,将退出循环
不要把通道当普通队列使用
由于管道简单,是否可以用作普通的queue数据结构用于算法啥的?
也没有绝对的理由不能用,但是通道就是给多goroutine用的,A goroutine写,B goroutine读才是王道
为通道声明方向
上文说到,对于同一个管道,在一个goroutine中应该要么收、要么发,建议不要既收也发而作为队列使用
例如:
func my_sender(ch chan int) {
ch <- xxx
}
func my_receiver(ch chan int) {
x := <-ch
}
my_sender仅作为发送者,my_receiver仅作为接收者
但是,在my_sender中接收ch,在my_receiver中向ch发送,都是被允许的:
func my_sender(ch chan int) {
ch <- xxx
x := ch //允许
}
func my_receiver(ch chan int) {
x := <-ch
ch <- x //允许
}
为了防止写错,Golang支持管道作为函数参数时的方向声明:
chan<- T
表示仅可发送,在此管道上接收将导致编译错误<-chan T
表示仅可接收,在此管道上发送将导致编译错误
//声明ch在此函数中仅可发送
func my_sender(ch chan<- int) {
ch <- xxx
x := ch //compile error!
}
//声明ch在此函数中仅可接收
func my_receiver(ch <-chan int) {
x := <-ch
ch <- x //compile error!
}
func main() {
ch := make(chan int, 3)
go my_sender(ch)//调用时没啥区别
go my_receiver(ch)//调用时没啥区别
}
使用通道时,在goroutine函数参数声明上加上方向声明是个非常好的习惯!
goroutine泄漏
两种情况:
- 协程从ch读数据,但总是无人往此ch写数据;读数据协程将永远阻塞下去
- 协程往ch写数据,但总是无人读取此ch;写数据一旦阻塞,将永远阻塞下去
协程永远阻塞,无法回收,于是乎:协程泄漏
例子:
func trySpeed() {
ch := make(char string)
go func() { ch <- 获取"baidu.com"响应数据 }()
go func() { ch <- 获取"google.com"响应数据 }()
go func() { ch <- 获取"127.0.0.1:80"响应数据 }()
return <-ch
}
trySpeed函数将读取ch通道,得到最快被访问的网址内容
由于ch是无缓冲管道,故另外两个协程访问到内容后写ch将阻塞
而由于trySpeed函数运行完后,ch管道也没人能看到了,于是乎,将无人再读取ch,于是剩余两个协程将永远阻塞 =》他俩泄漏了!