sunwenfei
  • 关于我
  • Golang
    • Goglang基础教程【译】
      • 介绍
        • 安装
        • Hello World
      • 变量、基本类型以及常量
        • 变量
        • 基本类型
        • 常量
      • 函数和包
        • 函数
        • 包
      • 条件、循环流程控制语句
        • if else条件语句
        • switch语句
        • 循环语句
      • 数组、切片、变参函数
        • 数组(Array)
        • 切片(Slice)
        • 变参函数
      • 其他数据类型
        • 映射(Map)
        • 字符串
      • 指针、结构体和方法
        • 指针
        • 结构体
        • 方法
      • 面向对象编程
        • 结构体 vs 类
        • 组合 vs 继承
        • 接口
        • 多态
      • 并发
        • 并发介绍
        • 协程(goroutine)
        • 管道(channel)
        • 带缓存的管道(buffered channel)
        • 协程池
        • 管道选择器(select)
        • 互斥锁(Mutex)
      • Defer
      • 一等公民函数
      • 反射
      • 错误
        • 错误处理
        • 自定义错误类型
        • panic和recover
      • 文件读写
        • 读文件
        • 写文件
    • Golang面向对象编程
    • Golang函数式编程
    • Golang并发编程
    • Golang web服务编程
    • Golang数据结构与算法
  • Shell编程
    • Find命令
  • JavaScript
    • browser
    • Node.JS
    • Deno
  • TypeScript
  • HTTP
    • 【译】通过信鸽理解HTTPS交互原理
  • React
    • React16
      • Hooks
        • 使用React Hooks拉取数据
  • 移动端开发
    • 原生
    • Flutter
    • ReactNative
    • 小程序
  • 前端测试
Powered by GitBook
On this page

Was this helpful?

  1. Golang
  2. Goglang基础教程【译】
  3. 并发

协程(goroutine)

上一节我们介绍了并发与并行的区别,我们知道了Golang是一种并发编程语言。本节我们就来看下Golang是如何利用goroutine,即协程来实现并发的。

什么是goroutine(协程)

goroutine指的是并发执行的函数/方法,这里的函数/方法我们可以理解为任务。goroutine也可看作是轻量级的线程,创建goroutine的开销要远远小于线程,在Golang中创建成千上万个goroutine是常有的事情。

goroutine vs 线程

  • goroutine开销更小。goroutine仅占用几kb的内存开销,而且可以根据需要动态缩放,而线程占用的内存空间则更大,而且是固定不变的。

  • goroutine是在线程基础上的一层抽象封装,所有创建的的goroutine本质上是复用少量的若干个线程。比如可能只有一个线程,成千上万个goroutine都跑在这个线程上,分时复用。假设其中一个goroutine阻塞了该线程的执行,比如在等待用户输出,那么Golang运行时会帮我们再创建一个系统级线程,将所有其他goroutine切到新创建的线程上去。这些虽然听起来略显复杂,但是一般情况下我们无需关注这些底层的调度,Golang为我们提供好了方便易用的API,比如goroutine来实现并发编程。

  • goroutine通过channel,即管道来实现goroutine之间的通信。channel能帮我们解决共享内存冲突的问题,这也是相对于线程的一个优势。我们下一节会详细介绍channel。

开启一个goroutine

开启一个goroutine的方式很简单,只需要在调用的函数/方法前加上关键字go即可。

下面我们创建一个goroutine:

package main

import "fmt"

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    fmt.Println("main function")
}

这里go hello()即开启了一个新的goroutine,然后hello()这个任务和会和main()任务并发执行。main()函数是默认goroutine,我们一般称作主协程。多运行该程序几次你会发现有可能输出三种不同的结果,分别是:

main function
Hello world goroutine
main function
main function
Hello world goroutine

是不是有点惊讶?之所以会是这样的结果就是因为go hello()创建的协程和主协程是并发执行的,谁都有可能先执行完。这就好比两个人赛跑,谁先跑到终点都不一定。我们修改下前面的程序:

package main

import (
    "fmt"
    "time"
)

func hello() {
    fmt.Println("Hello world goroutine")
}
func main() {
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

我们通过time.Sleep(1 * time.Second)来阻塞了主协程的执行,这时go hello()开启的协程便在主协程结束之前有足够多的时间去执行。该程序先输出Hello world goroutine,然后暂停了大约1秒钟,接着输出main function。

这里我们使用time.Sleep虽然能保证程序执行结果的一致性,但是真实应用中并没有这么用的,这里仅仅是为了演示子协程和主协程的同步问题。正规的做法是使用channel,即管道。这个后面一节再详细介绍。

开启多个goroutine

下面我们再通过一个例子加深对goroutine的理解,这次我们来开启多个goroutine。

package main

import (
    "fmt"
    "time"
)

func numbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

该程序创建了两个子协程,这两个子协程并发执行。numbers协程每间隔250ms打印一个数字;alphabets协程每间隔400ms打印一个字母。主协程等待3000ms打印main terminated然后退出。该程序执行结果如下,而且是稳定输出:

1 a 2 3 b 4 c 5 d e main terminated

下图形象的说明了该程序的执行过程:

Previous并发介绍Next管道(channel)

Last updated 5 years ago

Was this helpful?