go语言之Goroutines

时间秒杀一切 提交于 2020-03-16 17:55:15

说明

计算机每开启一个程序,都会开启一个进程,而每一个进程当中也同样会包含很多的子任务,这些任务会通过线程来分化完成,但是线程在切换的时候存在上下文切换消耗,这个时候就轮到协程登场了。

首先,我们要知道的是,协程是属于线程的,也就是说协程是在线程中跑的,因此协程用被称为微线程。因为协程需要手动切换所以更加的灵活被称为用户线程空间。

而在go中,天生就带有了goroutine,能够帮助我们完成并发操作,这也是go能够流行的原因。

Goroutines 是在 Golang 中执行并发任务的方式。它们仅存在于 Go 运行时的虚拟空间中而不存在于 OS 中,因此需要 Go 调度器来管理它们的生命周期。请记住这一点很重要,对于所有操作系统看到的都只有一个请求并运行多个线程的单个用户级进程。goroutine 本身由 GoRuntimeScheduler 管理。

关于goroutine的详细原理,可以去读一下go语言实战。

使用

在Go语言中,每一个并发的执行单元叫作一个goroutine。设想这里的一个程序有两个函数,一个函数做计算,另一个输出结果,假设两个函数没有相互之间的调用关系。一个线性的程序会先调用其中的一个函数,然后再调用另一个。如果程序中包含多个goroutine,对两个函数的调用则可能发生在同一时刻。马上就会看到这样的一个程序。

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。

f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

下面我们会来创建一个例子,main goroutine将计算菲波那契数列的第45个元素值。由于计算函数使用低效的递归,所以会运行相当长时间,在此期间我们想让用户看到一个可见的标识来表明程序依然在正常运行,所以来做一个动画的小图标:

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)
}

动画显示了几秒之后,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\n"))
        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\n"))
		if err != nil {
			return
		}
		time.Sleep(1 * time.Second)
	}

}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!