Go语言编程笔记7:goroutine和通道
goroutine
Python中并发的核心概念是协程,Go语言中类似的概念叫做goroutine。虽然两者在原理和使用方式等方面都有很大不同,但都是用于解决并发问题的核心概念。
协程(coroutine)与goroutine从名称上看就很相似。
我们知道,Python因为有全局线程锁的缘故,除了发生I/O的部分以外,大部分使用协程实现并发的时候实际上都是单线程在执行,事实上并不能挖掘多线程的全部性能,对于I/O密集型的应用的确是可以解决问题,但对于计算密集型的应用就无能为力了。
但是goroutine则不然,它更像是传统的多线程编程,在概念和功能上都与传统概念的“线程”更相似。
我们看这个示例代码:
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 218.
// Spinner displays an animation while computing the 45th Fibonacci number.
package main
import (
"fmt"
"time"
)
//!+
func main() {
go spinner(100 * time.Millisecond)
const n = 45
fibN := fib(n) // slow
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
func spinner(delay time.Duration) {
for {
for _, r := range `-\|/` {
fmt.Printf("\r%c", r)
time.Sleep(delay)
}
}
}
func fib(x int) int {
if x < 2 {
return x
}
return fib(x-1) + fib(x-2)
}
//!-
示例代码来自于《Go程序设计语言》。
主程序main
通过调用递归函数fib
来计算斐波那契数列,这是一个相当消耗时间的过程,在执行计算前,通过go spinner(100 * time.Millisecond)
语句开启了一个额外的goroutine,用于在屏幕上输出一个“滚动的光标”。
这个“滚动的光标”是利用“回车符”
\r
来实现的,当字符终端接收到\r
字符后,光标会移动到当前行的首部,并且清除当前行的内容,设定好时间间隔依次在\r
后输出-\|/
中的字符时,看起来就好像是一个光标在进行滚动一样。需要注意的是,只有在真正的字符终端(cmd或powershell)下才能看到这种效果,在VSC中输出结果时\r
是不起作用的。
当主线程计算完毕并退出程序后,负责输出光标的额外goroutine也会被强制关闭。
实际上,goroutine可以看做是Go语言在操作系统的线程概念之上封装的一个Go线程,由Go语言自己来控制goroutine的调度和切换,以达到一个不错的多线程执行效率,而不像操作系统线程那样依赖操作系统进行调度。事实上,Go语言编写的程序,都是以goroutine为单位进行执行的,作为程序入口的main
函数所在的goroutine,可以看做是主goroutine,通过主goroutine我们可以开启其它的goroutine,方式也很简单,就是示例中那样,通过go
关键字:go spinner(100 * time.Millisecond)
。这样可以简单地开启一个额外的goroutine,并在其中执行某个函数。此外,在主goroutine退出时,其它goroutine也会被关闭。
时钟服务器
《Go程序设计语言》中展示了一个时钟服务器作为示例:
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 219.
//!+
// Clock1 is a TCP server that periodically writes the time.
package main
import (
"fmt"
"io"
"log"
"net"
"time"
)
func main() {
listener, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatal(err)
}
for {
fmt.Println("server is listening...")
conn, err := listener.Accept()
if err != nil {
log.Print(err) // e.g., connection aborted
continue
}
fmt.Printf("get a request from %s \n", conn.RemoteAddr().String())
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\n"))
if err != nil {
return // e.g., client disconnected
}
time.Sleep(1 * time.Second)
}
}
//!-
这个例子会创建一个简单的网络服务,开启一个对本机8000
端口监听的TCP连接,如果有客户端访问,就会将当前时间回写到客户端进行输出。
在实际运行这个示例时,我发现有一些问题需要注意:
- 我这里使用的是WSL中的nc工具作为TCP客户端,通过WSL访问外部Windows的网络应用并不能直接
nc localhost 8000
,这是不可以的,必须通过一个虚拟网络中分配给外部Windows的IP才行。可以通过cat /etc/resolv.conf
命令查看该IP,详情可以阅读从 Linux(主机 IP)访问 Windows 网络应用。 - 如果客户端需要请求的IP不是
localhost
或127.0.0.1
(比如我在WSL中的请求),则服务端的Go程序中不能指定监听地址,比如:listener, err := net.Listen("tcp", "localhost:8000")
。否则服务端是不会响应客户端请求的,必须省略主机地址,比如net.Listen("tcp", ":8000")
,这样就会对客户端进行响应。 - 为了显示服务端运行情况和响应,我添加了一些显示代码。
对服务端代码编译运行后,就可以正常接收客户端请求:
❯ .\clock.exe
server is listening...
get a request from 172.23.244.190:46202
server is listening...
get a request from 172.23.244.190:46204
server is listening...
get a request from 172.23.244.190:46206
server is listening...
客户端请求的输出效果是这样的:
icexmoon@icexmoon-book:/mnt/c/Users/70748$ nc 172.23.240.1 8000
16:31:04
16:31:05
16:31:06
16:31:07
16:31:08
需要注意的是,当前服务端仅有一个主goroutine,所以一次仅能服务一个客户端,和一个客户端建立连接后就不会对其它客户端进行响应了,除非当前客户端断开连接。这点可以通过打开多个终端对服务器请求来进行验证。
当然,如果要能同时服务多个客户端,修改起来也很容易:
go handleConn(conn) // handle one connection at a time
只要启动一个额外的goroutine来服务当前客户的请求即可,主goroutine会继续for
循环,以监听其它的可能请求,具体的测试过程这里不再展示。
回声服务器
《Go程序设计语言》中还展示了一个有趣的回声服务器:
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 223.
// Reverb1 is a TCP server that simulates an echo.
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
"time"
)
//!+
func echo(c net.Conn, shout string, delay time.Duration) {
fmt.Fprintln(c, "\t", strings.ToUpper(shout))
time.Sleep(delay)
fmt.Fprintln(c, "\t", shout)