Go 语言入门很简单:从 goroutine 出发到并发

简介: goroutine 是 Go 中最基本的组织单位之一,是 Go 支持原生并发最重要的一个功能。事实上,每个 Go 程序至少拥有一个:main gotoutine ,当程序开始时会自动创建并启动。简单来说,gotoutine 是一个并发的函数(记住:不一定是并行)和其他代码一起运行。

goroutine

goroutine 是 Go 中最基本的组织单位之一,是 Go 支持原生并发最重要的一个功能。


事实上,每个 Go 程序至少拥有一个:main gotoutine ,当程序开始时会自动创建并启动。


简单来说,gotoutine 是一个并发的函数(记住:不一定是并行)和其他代码一起运行。


并发和并行的区别:并行机制指某个类型的多个实体同步执行,而并发是一种构建组件的方法,并在必要时能够独立地执行这些组件。


你可以简单的通过将 go 关键字放在函数前面来启动它:

func main() {
  go sayHello()
}
func sayHello() {
  fmt.Println("hello")
}


对于匿名函数,同样也能这么干,从下面这个例子你可以看得很明白。在下面的例子中,我们不是从一个函数建立一个 goroutine ,而是从一个匿名函数创建一个 goroutine :

go func() {
fmt.Println("hello")
}()// 1
// continue doing other things
//注意这里的(),我们必须立刻调用匿名函数来使go关键字有效。或者,你可以将函数分配给一个变量,并像这样调用它:
sayHello := func() {
  fmt.Println("hello")
}
go sayHello()
// continue doing other things


看起来很简单,对吧。我们可以用一个函数和一个关键字创建一个并发逻辑块,这就是启动 goroutine 所需要知道的全部。当然,关于如何正确使用它,对它进行同步以及如何组织它还有很多需要说明的内容。

goroutine 及它是如何工作的

本节将回答下面三个问题:goroutine 实际上是如何工作的? goroutine 是操作系统线程吗?还是说 goroutine 是绿色线程吗?我们可以创建多少个 goroutine 呢?


goroutines 对 Go 来说是独一无二的(尽管其他一些语言有类似的并发原语)。它们不是操作系统线程,它们不完全是绿色的线程(由语言运行时管理的线程),它们是更高级别的抽象,被称为协程(coroutines)。协程是非抢占的并发子程序,也就是说,它们不能被中断。


Go 的独特之处在于 goutine 与 Go 的运行时深度整合。Goroutine 没有定义自己的暂停或再入点; Go 语言在运行时观察着 goroutine 的行为,并在阻塞时自动挂起它们,然后在它们变畅通时恢复它们。在某种程度上,这使得它们可以抢占,但只是在 goroutine 被阻止的地方。它是运行时和 goroutine 逻辑之间的一种优雅合作关系。 因此,goroutine 可以被认为是一种特殊的协程。

什么是协程

协程,可以被认为是轻量级的线程。与线程不同的是,操作系统内核感知不到协程的存在。


协程的管理依赖 Go 语言运行时自身提供的调度器。也可以被认为是 goroutine 的隐式并发构造,但并发并非协程自带的属性:调度器必须能够同时托管几个协程,并给每个协程执行的机会,否则它们无法实现并发。当然,有可能有几个协程按顺序执行,但看起来就像并行一样。


协程是用户态的,Go 语言的协程从属于某个线程。Go 调度器通过多路复用技术,实现了所谓的 M:N 调度器,这意味着它将 M 个协程映射到 N 个系统线程。Go 调度器表示为负责 Go 程序中协程的执行方式和顺序的 Go 组件,Go 程序中的一切事物都将作为一个协程而执行。

创建一个协程

通过常规函数和匿名函数可以创建一个 goroutine,如新建一个 main.go 文件,写入如下代码:

package main
import (
  "fmt"
  "time"
)
func printNum() {
  for i := 0; i < 10; i++ {
    fmt.Print(i)
  }
}
func main() {
  go printNum()  // 常规函数创建协程
  // 匿名函数闯将协程
  go func() {
    for i := 10; i < 20; i++ {
      fmt.Print(i, " ")
    }
  }()
  time.Sleep(1 * time.Second)
  fmt.Println()
}

运行该代码,可能每次得到的结果还不一眼:

[Running] go run "/Users/yuzhou_1su/GoProjects/Go 并发/v1/main.go"
010 11 12 13 14 15 16 17 18 19 123456789
[Running] go run "/Users/yuzhou_1su/GoProjects/Go 并发/v1/main.go"
01210 11 12 13 14 15 16 17 18 19 3456789


每次运行结果不同说明了如果缺少额外对协程的操作,就无法控制协程的执行顺序。

创建多个协程

通过 flag 读取命令行动态数量的协程,比如通过 flag.Int 来读取命令行选项值,进而确定要创建的协程的数量。如果不传入值,n 的值将会变成 10.


利用 for 循环生成所需的协程数量,协程运行时很快的,只有通过 time.sleep() 语句赋予线程足够的时间结束其任务,以便我们能在命令行输出最终结果。如果不使用这条语句,可能会看不到完整的结果,读者可以自己试试。

package main
import (
  "flag"
  "fmt"
  "time"
)
func main() {
  n := flag.Int("num", 10, "Number of goroutines")
  flag.Parse()
  count := *n
  fmt.Printf("Going to create %d goroutine.\n", count)
  for i := 0; i < count; i++ {
    go func(x int) {
      fmt.Printf("%d ", x)
    }(i)
  }
  time.Sleep(time.Second)
  fmt.Println("\nExiting...")
}


运行该代码:

$ go run main.go -num 100
Going to create 100 goroutine.
5 3 4 0 29 17 18 19 16 6 7 8 9 10 11 12 13 14 15 20 1 22 21 44 55 30 32 33 34 35 36 37 38 39 2 23 41 27 24 52 26 47 31 42 25 28 43 40 70 54 45 46 66 67 56 60 68 62 59 72 76 78 58 82 83 50 80 91 96 61 90 98 77 87 49 73 74 53 79 75 65 48 84 63 94 71 51 92 64 93 89 85 97 69 88 86 57 99 95 81 
Exiting...
$ go run main.go -num 100
Going to create 100 goroutine.
13 7 8 9 10 11 12 0 1 36 14 15 16 17 18 19 20 3 6 4 5 28 22 23 24 21 27 30 31 33 29 2 34 48 25 57 35 37 49 50 51 52 53 54 55 56 42 39 38 32 44 40 74 89 58 75 87 47 60 94 26 64 41 92 85 67 81 46 43 88 83 77 80 90 63 91 84 96 59 65 99 66 86 93 79 76 61 98 68 69 82 45 78 72 70 71 62 73 95 97 
Exiting...

总结

goroutine 是 Go 语言中的轻量级线程实现,也即协程,由 Go 调度器在运行时管理,有两种方式创建 goroutine,一种是直接在常规函数前加上关键字 go,或者声明一个匿名函数。


运行时被 go 声明的 goroutine 会在新的 goroutine 中并发执行,当被调用的函数返回时,这个 goroutine 也就自动结束了。


值得注意的是,多个协程一般没有先后顺序,只有通过一定得方式(比如通道、锁等)来限定顺序。


在学习过程中,我们会利用 time.Sleep 来等待协程运行完输出,实际上并不需要时间的延迟。


最后,学习会 goroutine 只是学习 Go 并发的第一步,后续还有更多的并发知识点等着我们去探索,下一篇文章见!

相关文章
|
7天前
|
存储 前端开发 Go
Go语言中的数组
在 Go 语言中,数组是一种固定长度的、相同类型元素的序列。数组声明时长度已确定,不可改变,支持多种初始化方式,如使用 `var` 关键字、短变量声明、省略号 `...` 推断长度等。数组内存布局连续,可通过索引高效访问。遍历数组常用 `for` 循环和 `range` 关键字。
|
4天前
|
存储 安全 算法
Go语言是如何支持多线程的
【10月更文挑战第21】Go语言是如何支持多线程的
101 72
|
4天前
|
安全 大数据 Go
介绍一下Go语言的并发模型
【10月更文挑战第21】介绍一下Go语言的并发模型
24 14
|
3天前
|
安全 Go 开发者
go语言并发模型
【10月更文挑战第16天】
18 8
|
4天前
|
Java 大数据 Go
Go语言:高效并发的编程新星
【10月更文挑战第21】Go语言:高效并发的编程新星
17 7
|
3天前
|
安全 Java Go
go语言高效切换
【10月更文挑战第16天】
12 5
|
3天前
|
运维 监控 Go
go语言轻量化
【10月更文挑战第16天】
10 3
|
3天前
|
安全 Go 调度
Go语言中的并发编程:解锁高性能程序设计之门####
探索Go语言如何以简洁高效的并发模型,重新定义现代软件开发的边界。本文将深入剖析Goroutines与Channels的工作原理,揭秘它们为何成为实现高并发、高性能应用的关键。跟随我们的旅程,从基础概念到实战技巧,一步步揭开Go并发编程的神秘面纱,让您的代码在多核时代翩翩起舞。 ####
|
4天前
|
Go 调度 开发者
Go语言多线程的优势
【10月更文挑战第15天】
10 4
|
5天前
|
存储 Go 开发者
Go语言中的并发编程与通道机制
本文将探讨Go语言中并发编程的核心概念——goroutine和通道(channel)。我们将从基础开始,解释什么是goroutine以及如何创建和使用它们。然后,我们将深入探讨通道的概念、类型以及如何使用通道在goroutine之间进行通信。最后,我们将通过一个示例来展示如何在实际应用中使用goroutine和通道来实现并发编程。