Go语言编程笔记7:goroutine和通道

Go语言编程笔记7:goroutine和通道

image-20211108153040805

图源:wallpapercave.com

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不是localhost127.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)
	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值