go 并发
说明
有人把go比作是21世纪的c语言,第一是因为go的设计比较简单,第二,21世纪最重要的就是并发程序设计,而go从语言层面就支持并发。
与此同时,并发程序的内存管理是非常复杂的,而在go中提供了垃圾回收的机制。
Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。
Go语言中的并发程序主要使用两种手段来实现。goroutine和channel。
goroutine
goroutine是go并发编程的核心,说到底,goroutine就是协程,比线程更小,go语言在内部帮忙实现了goroutine之间的内存共享。执行goroutine只需要极少的栈内存(大概4-5kb),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
一般情况下,一个普通计算机跑几十个线程就有点负载过大了,但是同样的机器却可以轻松地让成百上千个goroutine进行资源竞争。
使用
想要创建一个goroutine,只需要在普通函数的前面加一个go关键字,就可以创建并发执行单元。
在并发编程中,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。而go语言的并发设计,让我们很轻松就可以达成这一目的。
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个goroutine
go func() {
for i:=0;i<10;i++ {
fmt.Printf("子goroutine输出%d
",i)
time.Sleep(time.Second * 2)
}
}()
for i:=0;i<10;i++ {
fmt.Printf("主main goroutine输出%c
",i+'a')
time.Sleep(time.Second * 2)
}
}
输出结果如下:
主main goroutine输出a
子goroutine输出0
子goroutine输出1
主main goroutine输出b
子goroutine输出2
主main goroutine输出c
子goroutine输出3
主main goroutine输出d
子goroutine输出4
主main goroutine输出e
子goroutine输出5
主main goroutine输出f
子goroutine输出6
主main goroutine输出g
子goroutine输出7
主main goroutine输出h
子goroutine输出8
主main goroutine输出i
主main goroutine输出j
子goroutine输出9
Process finished with exit code 0
特性
当主goroutine退出后,其他的goroutine也会退出。
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d
", i)
time.Sleep(1 * time.Second) //延时1s
}
}
func main() {
//创建一个 goroutine,启动另外一个任务
go newTask()
fmt.Println("main goroutine exit")
}
输出结果如下:
示例
下面我们会来创建一个例子,main goroutine将计算菲波那契数列的第45个元素值。由于计算函数使用低效的递归,所以会运行相当长时间,在此期间我们想让用户看到一个可见的标识来表明程序依然在正常运行,所以来做一个动画的小图标:
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("
Fibonacci(%d) = %d
", n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-|/` {
fmt.Printf("
%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
动画显示了几秒之后,fib(45)的调用成功地返回,并且打印结果:
Fibonacci(45) = 1134903170
然后主函数返回。主函数返回时,所有的goroutine都会被直接打断,程序退出。
下面我们再来看一个例子,是一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:
package main
import (
"io"
"log"
"net"
"time"
)
func main() {
listener, err := net.Listen("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // e.g., connection aborted
continue
}
handleConn(conn) // handle one connection at a time
}
}
func handleConn(c net.Conn) {
defer c.Close()
for {
_, err := io.WriteString(c, time.Now().Format("15:04:05
"))
if err != nil {
return // e.g., client disconnected
}
time.Sleep(1 * time.Second)
}
}
在上面的代码中,我们通过Listener对象监听网络端口上的连接,上面的代码中是通过监听tcp的localhost:8000端口,同时Accept会阻塞程序,直到一个新的连接被创建,然后会返回一个net.Conn对象表示连接。
handleConn函数会处理一个完整的客户端连接。在一个for死循环中,用time.Now()获取当前时刻,然后写到客户端。由于net.Conn实现了io.Writer接口,我们可以直接向其写入内容。这个死循环会一直执行,直到写入失败。最可能的原因是客户端主动断开连接。这种情况下handleConn函数会用defer调用关闭服务器侧的连接,然后返回到主函数,继续等待下一个连接请求。
将上面的代码运行完毕之后,来通过nc(netcat)来执行网络连接。
$ go build gopl.io/ch8/clock1
$ ./clock1 &
$ nc localhost 8000
13:58:54
13:58:55
13:58:56
13:58:57
^C
通过上面的代码我们完成了上述的需求,但是此时只能满足一个人的连接,如果两个或者两个以上的人来连接,那么就只能第一个人退出连接后,后面的人才能进入连接。
如果这个时候想要实现多人连接同时打印时间,我们可以将我们的代码稍微的改变一下,变成使用goroutine就可以实现需求,如下:
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // e.g., connection aborted
continue
}
go handleConn(conn) // handle connections concurrently
}
通过上面的代码,就可以实现我们上面的需求,每一个客户端连接,就会在主main goroutine之外创建一个goroutine,这样就可以实现大家同时打印时间的效果。
接下来我们在把需求深入一下,在执行代码的时候,允许添加端口和时区,这样当用户连接的时候,就可以获取当前时区的时间。实现的代码如下:
package main
import (
"flag"
"io"
"log"
"net"
"time"
)
var port = flag.String("port","8000","请输入端口号")
func main() {
flag.Parse()
listener,err := net.Listen("tcp","localhost:"+*port)
if err != nil {
log.Fatal(err)
}
for {
conn,err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
go handleConnect(conn)
}
}
func handleConnect(c net.Conn) {
// 关闭
defer c.Close()
for {
_,err := io.WriteString(c,time.Now().Format("15:04:05
"))
if err != nil {
return
}
time.Sleep(1 * time.Second)
}
}
代码执行:
runtime包
Gosched
runtime.Gosched() 用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次再获得cpu时间轮片的时候,从该出让cpu的位置恢复执行。
有点像跑接力赛,A跑了一会碰到代码runtime.Gosched() 就把接力棒交给B了,A歇着了,B继续跑。
下面的代码是没有使用runtime.Gosched()的代码:
package main
import (
"fmt"
)
func main() {
go func(str string) {
for _,val := range str {
fmt.Printf("子goroutine:%c
",val)
}
}("hello")
for _,val := range "world" {
fmt.Printf("主goroutine:%c
",val)
}
}
输出结果如下:
子goroutine:h
子goroutine:e
子goroutine:l
子goroutine:l
子goroutine:o
主goroutine:w
主goroutine:o
主goroutine:r
主goroutine:l
主goroutine:d
Process finished with exit code 0
下面让主goroutine使用runtime.Gosched让出cpu时间片。如下:
package main
import (
"fmt"
"runtime"
)
func main() {
go func(str string) {
for _,val := range str {
fmt.Printf("子goroutine:%c
",val)
}
}("hello")
for _,val := range "world" {
runtime.Gosched() // 让出cpu时间片
fmt.Printf("主goroutine:%c
",val)
}
}
当让出时间片,输出的结果为:
子goroutine:h
子goroutine:e
子goroutine:l
子goroutine:l
子goroutine:o
主goroutine:w
主goroutine:o
主goroutine:r
主goroutine:l
主goroutine:d
Process finished with exit code 0
当让出cpu时间片后,就会先执行子goroutine,然后在执行主goroutine。
Goexit
调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer延迟调用被执行。
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 终止当前 goroutine, import "runtime"
fmt.Println("B") // 不会执行
}()
fmt.Println("A") // 不会执行
}() //不要忘记()
//死循环,目的不让主goroutine结束
for {
}
}
程序运行结果:
GOMAXPROCS
调用 runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值.
package main
import (
"fmt"
"runtime"
)
func main () {
// 设置cpu核数
// 括号内的参数表示设置的最大cpu核数
// 返回值表示上一次设置的cpu核数
n1 := runtime.GOMAXPROCS(1) // 第一次调用返回默认的核数 4
fmt.Println(n1)
n2 := runtime.GOMAXPROCS(2) // 第二次调用返回上一次设置的核数 1
fmt.Println(n2)
}
运行结果如下:
4
1
Process finished with exit code 0