Go goroutine

在Java或C++中实现并发编程时,通常需要自己维护一个线程池,并需要包装一个又一个的任务去调度线程执行任务同时还需要维护上下文切换,这一切通常会耗费开发人员大量心智。能不能有一种机制,开发人员只需要定义任务,让系统去将任务分配到CPU上实现并发执行呢?

Go语言的goroutine就是这种机制,goroutine的概念类似于线程,不同之处在于goroutine由Go程序运行时调度和管理。Go程序会智能地将goroutine中的任务合理地分配给每个CPU。

Go语言之所以称为现代化编程语言,是因为它在语言层面上内置了调度和上下文切换的机制。因此在Go并发编程中开发人员无需自己编写进程、线程、协程,只需要编写goroutine让任务并发执行。

并发 & 并行

简单来说,并发是在同一时间处理多件事情,并行是在同一时间做多件事情。并发的目的在于把当个CPU的利用率使用到最高,而并行则需要多核CPU的支持。

  • 并发:逻辑上具备指同一时间段内同时执行多个任务的能力
  • 并行:物理上同一时刻执行多个并发任务

Go语言最高特色之一是支持高并发编程模型,以goroutine作为基本的执行单元。

goroutine

Go语言使用go关键字开启goroutinue来支持并发编程,goroutine是轻量级线程,goroutine的调度是由Go运行时进行管理的。使用go语句开启一个全新的运行期线程,即goroutine。会以一个不同的且全新的goroutine来执行一个函数。同一个函数中所有goroutine将共享同一个地址空间。

例如:创建并发执⾏单元,Go调度器会将其安排到合适的系统线程上去执行。

go funcName(argumentList)

注意函数前添加go关键字是创建并发任务,而非执行并发操作。新建的并行任务单元会被放置到系统队列中,等待调度器安排合适的系统线程去获取执行权。

Go程序中使用go关键字为一个函数创建一个goroutine,一个函数可以创建出多个goroutine,而一个goroutine必定对应一个函数。启动goroutine只需在调用函数(普通函数或匿名函数)前添加go关键字。

使用匿名函数创建goroutine

go关键字可以为匿名函数或闭包启动goroutine

go func(argumentList) {
  //function body
}(parameters)

例如:并行执行定时打印计数

package main

import (
    "fmt"
    "time"
)

func main() {
    //并发执行计数器
    go func() {
        var count int
        for {
            count++
            fmt.Println("tick", count)
            time.Sleep(time.Second)
        }
    }()
    //接收命令行输入
    var input string
    fmt.Scanln(&input)
}

并行

例如:计数器每秒执行1次的同时等待用户输入

package main

import (
    "fmt"
    "time"
)

//每隔1秒打印依次计数器
func counter() {
    var count int
    //无限循环
    for {
        count++
        fmt.Println("tick", count)
        time.Sleep(time.Second) //延迟1秒
    }
}

func main() {
    //并发执行计数器
    go counter()
    //接收命令行输入
    var input string
    fmt.Scanln(&input)
}

Go程序启动时运行时runtime会默认为main()主函数创建一个goroutine,在main()主函数的goroutine中执行go counter()时,counter()计数器函数的goroutine被创建。counter()函数在自己的goroutine中执行。同时main()主函数依然持续运行。两个goroutine通过Go程序的调度机制同时在运行。

串行

在Windows和Linux出现之前的古老年代,程序员在开发时并没有并发的概念,因为命令式程序设计语言是以串行为基础的,程序会顺序执行的每一条指令,整个程序只有一个执行上下文,即一个调用栈和一个堆。

并发意味着程序在运行时具有多个执行上下文,对应着多个调用栈。由于每个进程在运行时都具有自己的调用栈和堆,因此会拥有一个完整的上下文。而操作系统在调度进程时会保存被调度进程的上下文环境,等待进程获得时间片后,再恢复该进程的上下文到系统中。

例如:默认Go程序是安装串行方式从上到下依次执行的

$ cd project
$ mkdir base && cd base
$ go mod init base
$ vim main.go
package main

import "fmt"

func test() {
    fmt.Println("hello test")
}
func main() {
    test()
    fmt.Println("hello main")
}
$ go run main.go
hell test
hello main

main goroutine & work goroutine

Go程序启动时会为main()函数创建一个默认的goroutinemain goroutine,当main()函数返回时main goroutine结束,所有在main()函数中启动的goroutine也会一起结束。

package main

import (
    "fmt"
)

func test() {
    fmt.Println("hell test")
}
func main() {
    go test()
    fmt.Println("hello main")
}
$ go run main.go
hello main

因为goroutine和线程一样,主函数main()goroutine并不会等待其它goroutine结束。如果main()函数中的goroutine结束,其下的所有goroutine都将结束。

需要注意的是,函数调用前添加go关键字表示本次调用会在一个全新的goroutine中并发执行,当被调用的函数return返回时,当前的work goroutine也会自动结束。如果函数存在返回值,则返回值会被丢弃。

goroutine & sleep

若将test()替换为go test()则此时会启动另外一个goroutine去执行test()函数,因此打印时只会出现hello main,而不会出现hello test。若想要打印出hello test简单粗暴的方式是使用time.Sleep

package main

import (
    "fmt"
    "time"
)

func test() {
    fmt.Println("hell test")
}
func main() {
    go test()
    fmt.Println("hello main")
    time.Sleep(time.Second)
}
$ go run main.go
hell test
hello main

main goroutine退出后,其它的work goroutine也会自动退出。

coroutine VS thread

Go语言的并发是通过goroutine来实现的,goroutine类似于线程,属于用户态的线程。Go语言是一个原生支持用户态线程goroutine的语言,可根据需要创建多个goroutine并发工作。与线程不同的是,goroutine是由Go语言的运行时调度完成的,而线程则是由操作系统调度完成的。

进程、线程、协程区别

  • 进程(process)
    进程拥有自己独立的堆和栈,即不共享堆也不共享栈,进程由操作系统调度。
  • 线程(thread)
    线程拥有自己独立的栈和共享的堆,即共享堆不共享栈,线程由操作系统调度。
  • 协程(coroutine)
    协程和线程一样共享堆但不共享栈,协程是由开发人员在编码中进行手工调度。

简单地将goroutine归纳为协程并不合适,goroutine会在运行时创建多个线程来执行并发任务,goroutine并发执行单元可被调度到其它线程中并行执行,因此更像是多线程和协程的综合体,能最大限度地提升执行效率,以发挥多核处理能力。

goroutine VS thread

goroutine是建立在线程之上的轻量级的抽象,goroutine允许以非常低的代价在同一个地址空间中并行第执行多个函数或方法。相比于线程,goroutine的创建和销毁的代价要小很多,而且它的调度是独立于线程的。

goroutine与线程之间的区别

首先goroutine并不会比线程运行得更快,goroutine只会增加更多的并发性。因为当一个goroutine被阻塞(比如等待I/O)时,Go的Scheduler会调度其它可以执行的goroutine继续运行。

相比于线程,goroutine优点在于

  • 内存消耗更少

goroutine所需的内存通常只有20KB,而线程则需要1MB,之间500倍差距。

  • 创建于销毁的开销更小

线程创建时需要向操作系统申请资源,销毁时必须将资源归还,因此创建和销毁时的开销更大。而goroutine的创建和销毁是由Go语言在运行时自己管理的,因此更低。

goroutine
  • 切换开销更小

Go实现高并发的主要原因由于切换开销更小,这也是与线程最主要的区别。线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占。在线程切换的过程中需要保存和恢复所有及寄存器信息,比如16个通用寄存器、PC(Program Counter)程序计数器、SP(Stack Pointer)堆栈指针、段寄存器等等。goroutine的调度是协同式的,不会直接与操作系统内核打交道。当goroutine进行切换时,只有少量的寄存器需要保存和恢复,因此切换效率更高。

goroutine是Go并行设计的核心,说到底其实是协程,比线程更小,十几个goroutine体现在底层可能就是五六个线程。Go语言内部实现了goroutine之间的内存共享。执行goroutine仅需极少的栈内存(大约4~5KB),会根据相应的数据来伸缩。因此,可同时运行成千上万的并发任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值