一天搞懂Go语言(5)——goroutine和通道
并发编程表现为程序由若干个自主的活动单元组成。go有两种并发编程风格,一种是goroutine和通道,它们支持通信顺序进程(CSP),CSP是一个并发模式,在不同的执行体(goroutine)之间传递值,但是变量本身局限于单一的执行体。还有一种共享内存多线程的传统模型,它们和在其他主流语言中使用线程类似。
goroutine
在go里面,每一个并发执行的活动称为goroutine,类似于线程。当一个程序启动时候,只有一个goroutine来调用main函数,称为主goroutine,新的goroutine通过go语句进行创建。
f() //调用f(),等待它返回 go f() //新建一个调用f()的grouting,不用等待
除了从main返回或者退出程序外,没有程序化的方法让一个goroutine来停止另一个,但是有办法和goroutine通信来要求它自己停止。
示例
并发时钟服务器
package main
import(
"io"
"log"
"net"
"time"
)
func handleConn(c net.Conn) {
defer c.Close()
for{
_,err := io.WriteString(c,time.Now().Format("15:04:05\n"))
if err != nil{
return //例如,连接断开
}
time.Sleep(1*time.Second)
}
}
func main() {
listener,err:=net.Listen("tcp","localhost:8080")
if err!=nil{
log.Fatal(err)
}
for{
conn,err:=listener.Accept() //阻塞,直到有连接请求进来
if err!=nil{
log.Print(err) //连接中止
continue
}
handleConn(conn) //一次处理一个连接
}
}
当运行这段程序,使用nc命令连接本地8080端口时候,客户端会显示每秒从服务器发送的时间。使用killall clock1来终止指定名字的进程。
当第二个客户端需要链接进来的时候必须等第一个客户端结束,因为服务器是顺序的,一次只能处理一个客户请求。让服务器支持并发只需要一个很小的改变:在调用handleConn的地方添加一个go关键字,使它在自己的goroutine内执行。
for{
conn,err:=listener.Accept() //阻塞,直到有连接请求进来
if err!=nil{
log.Print(err) //连接中止
continue
}
go handleConn(conn) //并发处理连接
}
现在多个客户端可以同时接收到时间。
通道
如果说goroutine是Go程序并发的执行体,通道就是它们之间的连接,每一个通道是一个具体类型的导管,叫做通道的元素类型。
ch := make(chan int) //ch类型是‘chan int’ ,创建一个无缓冲通道 ch := make(chan int,3) //容量为3的缓冲通道
像map一样,通道是一个使用make创建的数据结构的引用。和其他引用类型一样,零值为nil。通道由主要两个操作,发送和接收。
ch <- x //发送语句 x = <-ch //接收语句 <-ch //接收,丢弃结果 //第三个操作close,将设置一个标志位来指示这个通道后面没有值了; close(ch)
关闭后的发送操作将导致宕机,而接收操作将获取所有已发送的值。
使用make创建的通道叫做无缓冲通道,后面还有一个可选参数表示通道的容量,如果为0(默认值),则创建一个无缓冲通道。
无缓冲通道
无缓冲通道,无论是发送操作还是接收操作都会阻塞,直到一方发送完毕或接收完毕。这样会导致两个goroutine同步化,因此无缓冲通道也叫同步通道。通过通道发送消息有一个值,有时候通信本身以及通信发生的时间也很重要,当我们强调这方面的时候,往往把消息称作事件。当事件没有携带额外消息,只是单纯的同步,我们通过使用一个struct{}元素类型通道强调它,尽管通常使用bool或int。
管道
多个goroutine通过通道连接起来,一个输出是另一个的输入,就组成了管道。下面是三个goroutine,产生整数、求平方、输出。
package main
import "fmt"
func main() {
naturals:=make(chan int)
squares:=make(chan int)
//counter
go func() {
for x:=0; ;x++{
naturals <- x
}
}()
//squares
go func() {
for{
x:=<-naturals
squares<-x*x
}
}()
//printer
for{
fmt.Println(<-squares)
}
}
如果发送方没有数据要发送,我们可以通过close关闭,所有发送操作会宕机,后续接收操作会获取零值。没有一个直接方式来判断通道是否关闭,但是有一个接收操作的变种,他产生两个结果:接收到的通道元素以及一个布尔值:true表示成功接收,false表示通道关闭。
//squares
go func() {
for{
x,ok:=<-naturals
if !ok{
break //通道关闭
}
squares<-x*x
}
close(squares)//通道读完关闭
}()
更为方便的是该语言提供了range循环语法以在通道上迭代,接受完最后一个值后关闭循环。
func main() {
naturals:=make(chan int)
squares:=make(chan int)
//counter
go func() {
for x:=0; ;x++{
naturals <- x
}
close(naturals)
}()
//squares
go func() {
for x:=range naturals{
squares<-x*x
}
close(squares)//通道读完关闭
}()
//printer
for x:=range squares{
fmt.Println(x)
}
}
结束时关闭每一个通道不是必须的,因为go语言可以通过垃圾回收器根据它是否可以访问来决定是否回收它,而不是根据它是否关闭。(和文件close操作不一样,关闭文件是必须的)。
单向通道
当一个通道用做函数的形参时,它几乎总是被有意地限制不能发送或接收。
chan<-int //只能发送的通道 <-chan int //只能接收的通道
因为close操作仅仅在发送方才能调用,所以试图关闭一个仅能接收的通道在编译时会报错。
func counter(out chan<-int) {
for x:=0;x<100;x++{
out<-x
}
close(out)
}
//...
func main() {
naturals:=make(chan int)
go counter(naturals)
//...
}
在任何赋值操作中,将双向通道转换为单向通道都是允许的,但反过来不行。
缓冲通道
缓冲通道使得发送方可以无阻塞的发送缓冲容量大小的数据,接收方相反。所以当缓冲通道满,发送操作会阻塞,缓冲通道空,接收操作会阻塞。
ch := make(chan int,3) cap(ch) //获取通道容量 len(ch) //获取通道元素个数
向一个没有goroutine在接收的通道上不断发送会发生goroutine泄漏。
select多路复用
select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。select随机执行一个可运行的case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行。
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
- 相关语法:
- 每个case都必须是一个通信
- 所有channel表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行,其他被忽略
- 如果有多个case都可以运行,select 会随机公平地选出一个执行。其他不会执行
- 如果没有通道可以执行,则:
- 如果有default子句,则执行该语句
- 如果没有default子句,select 将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值
取消
有时候我们需要让一个goroutine停止它当前的任务,一个goroutine无法直接终止另一个,因为这样会让所有的共享变量状态处于不确定状态。
当一个通道关闭且已取完所有发送的值后,接下来的接收操作会立即返回,得到零值。我们可以利用它创建一个广播机制:不在通道上发送值,而是关闭它。

浙公网安备 33010602011771号