在操作系统中,执行体是个抽象的概念。与之对应的实体有进程、线程以及协程(coroutine)。协程也叫轻量级的线程,与传统的进程和线程相比,协程的最大特点是 "轻"!可以轻松创建上百万个协程而不会导致系统资源衰竭。
多数编程语言在语法层面并不直接支持协程,而是通过库的方式支持。但是用库的方式支持的功能往往不是很完整,比如仅仅提供轻量级线程的创建、销毁和切换等能力。如果在这样的协程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行的协程,从而无法达到轻量级线程本身期望达到的目标。

goroutine

Golang 在语言级别支持协程,称之为 goroutine。Golang 标准库提供的所有系统调用操作(包括所有的同步 IO 操作),都会出让 CPU 给其他 goroutine。这让 goroutine 的切换管理不依赖于系统的线程和进程,也不依赖于 CPU 的核心数量,而是交给 Golang 的运行时统一调度。

goroutine 是 Golang 中并发设计的核心,更多关于并发的概念,请参考《Golang 入门 : 理解并发与并行》。 本文接下来的部分着重通过 demo 介绍 goroutine 的用法。

入门 demo

要在一个协程中运行函数,直接在调用函数时添加关键字 go 就可以了:

  1. package main
  2.  
  3. import (
  4. "time"
  5. "fmt"
  6. )
  7.  
  8. func say(s string) {
  9. for i := ; i < ; i++ {
  10. time.Sleep( * time.Millisecond)
  11. fmt.Println(s)
  12. }
  13. }
  14.  
  15. func main() {
  16. go say("hello world")
  17. time.Sleep( * time.Millisecond)
  18. fmt.Println("over!")
  19. }

执行上面的代码,输出结果为:

  1. hello world
  2. hello world
  3. hello world
  4. over!

至于为什么要在 main 函数中调用 Sleep,如何用优雅的方式代替 Sleep,请参考《Golang 入门 : 等待 goroutine 完成任务》一文。

goroutine 的生命周期

让我们通过下面的 demo 来理解 goroutine 的生命周期:

  1. package main
  2.  
  3. import (
  4. "runtime"
  5. "sync"
  6. "fmt"
  7. )
  8.  
  9. func main() {
  10. // 分配一个逻辑处理器给调度器使用
  11. runtime.GOMAXPROCS()
  12.  
  13. // wg用来等待程序完成
  14. // 计数加2,表示要等待两个goroutine
  15. var wg sync.WaitGroup
  16. wg.Add()
  17.  
  18. fmt.Println("Start Goroutines")
  19.  
  20. // 声明一个匿名函数,并创建一个goroutine
  21. go func(){
  22. // 在函数退出时调用Done来通知main函数工作已经完成
  23. defer wg.Done()
  24.  
  25. // 显示字母表3次
  26. for count := ; count< ; count++{
  27. for char := 'a'; char< 'a'+; char++{
  28. fmt.Printf("%c ", char)
  29. }
  30. fmt.Println()
  31. }
  32. }()
  33. // 声明一个匿名函数,并创建一个goroutine
  34. go func(){
  35. // 在函数退出时调用Done来通知main函数工作已经完成
  36. defer wg.Done()
  37.  
  38. // 显示字母表3次
  39. for count := ; count< ; count++{
  40. for char := 'A'; char< 'A'+; char++{
  41. fmt.Printf("%c ", char)
  42. }
  43. fmt.Println()
  44. }
  45. }()
  46.  
  47. // 等待goroutine结束
  48. fmt.Println("Waiting To Finish")
  49. wg.Wait()
  50.  
  51. fmt.Println("Terminating Program")
  52. }

在 demo 的起始部分,通过调用 runtime 包中的 GOMAXPROCS 函数,把可以使用的逻辑处理器的数量设置为 1。
接下来通过 goroutine 执行的两个匿名函数分别输出三遍小写字母和三遍大写字母。运行上面代码,输出的结果如下:

  1. Start Goroutines
  2. Waiting To Finish
  3. A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  4. A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  5. A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  6. a b c d e f g h i j k l m n o p q r s t u v w x y z
  7. a b c d e f g h i j k l m n o p q r s t u v w x y z
  8. a b c d e f g h i j k l m n o p q r s t u v w x y z
  9. Terminating Program

第一个 goroutine 完成所有任务的时间太短了,以至于在调度器切换到第二个 goroutine 之前,就完成了所有任务。这也是为什么会看到先输出了所有的大写字母,之后才输出小写字母。我们创建的两个 goroutine 一个接一个地并发运行,独立完成显示字母表的任务。
因为 goroutine 以非阻塞的方式执行,它们会随着程序(主线程)的结束而消亡,所以我们在 main 函数中使用 WaitGroup 来等待两个 goroutine 完成他们的工作,更多 WaitGroup 相关的信息,请参考《Golang 入门 : 等待 goroutine 完成任务》一文。

基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。
让我们通过下图来理解这一场景(下图来自互联网):

  • 在第 1 步,调度器开始运行 goroutine A,而 goroutine B 在运行队列里等待调度。
  • 在第 2 步,调度器交换了 goroutine A 和 goroutine B。由于 goroutine A 并没有完成工作,因此被放回到运行队列。
  • 在第 3 步,goroutine B 完成了它的工作并被系统销毁。这也让 goroutine A 继续之前的工作。

让我们通过一个运行时间长些的任务来观察该行为,运行下面的 代码:

  1. package main
  2.  
  3. import (
  4. "runtime"
  5. "sync"
  6. "fmt"
  7. )
  8.  
  9. func main() {
  10. // wg用来等待程序完成
  11. var wg sync.WaitGroup
  12.  
  13. // 分配一个逻辑处理器给调度器使用
  14. runtime.GOMAXPROCS()
  15.  
  16. // 计数加2,表示要等待两个goroutine
  17. wg.Add()
  18.  
  19. // 创建两个goroutine
  20. fmt.Println("Create Goroutines")
  21. go printPrime("A", &wg)
  22. go printPrime("B", &wg)
  23.  
  24. // 等待goroutine结束
  25. fmt.Println("Waiting To Finish")
  26. wg.Wait()
  27.  
  28. fmt.Println("Terminating Program")
  29. }
  30.  
  31. // printPrime 显示5000以内的素数值
  32. func printPrime(prefix string, wg *sync.WaitGroup){
  33. // 在函数退出时调用Done来通知main函数工作已经完成
  34. defer wg.Done()
  35.  
  36. next:
  37. for outer := ; outer < ; outer++ {
  38. for inner := ; inner < outer; inner++ {
  39. if outer % inner == {
  40. continue next
  41. }
  42. }
  43. fmt.Printf("%s:%d\n", prefix, outer)
  44. }
  45. fmt.Println("Completed", prefix)
  46. }

代码中运行了两个 goroutine,分别打印 1-5000 内的素数,输出的结果比较长,精简如下:

  1. Create Goroutines
  2. Waiting To Finish
  3. B:
  4. B:
  5. ...
  6. B:
  7. A: ** 切换 goroutine
  8. A:
  9. ...
  10. A:
  11. B: ** 切换 goroutine
  12. ...
  13. B:
  14. Completed B
  15. A: ** 切换 goroutine
  16. ...
  17. A:
  18. Completed A
  19. Terminating Program

上面的输出说明:goroutine B 先执行,然后切换到 goroutine A,再切换到 goroutine B 运行至任务结束,最后又切换到 goroutine A,运行至任务结束。注意,每次运行这个程序,调度器切换的时间点都会稍有不同。

让 goroutine 并行执行

前面的两个示例,通过设置 runtime.GOMAXPROCS(1),强制让 goroutine 在一个逻辑处理器上并发执行。用同样的方式,我们可以设置逻辑处理器的个数等于物理处理器的个数,从而让 goroutine 并行执行(物理处理器的个数得大于 1)。
下面的代码可以让逻辑处理器的个数等于物理处理器的个数:

  1. runtime.GOMAXPROCS(runtime.NumCPU())

其中的函数 NumCPU 返回可以使用的物理处理器的数量。因此,调用 GOMAXPROCS 函数就为每个可用的物理处理器创建一个逻辑处理器。注意,从 Golang 1.5 开始,GOMAXPROCS 的默认值已经等于可以使用的物理处理器的数量了。
修改上面输出素数的程序:

  1. runtime.GOMAXPROCS()

因为我们只创建了两个 goroutine,所以逻辑处理器的数量设置为 2 就可以了,重新运行该程序,看看是不是 A 和 B 的输出混合在一起了:

  1. ...
  2. B:
  3. B:
  4. A:
  5. A:
  6. B:
  7. A:
  8. A:
  9. A:
  10. A:
  11. A:
  12. B:
  13. A:
  14. ...

除了这个 demo 程序,在真实场景中这种并行的方式会带来很多数据同步的问题。接下来我们将介绍如何来解决数据的同步问题。

参考:
《Go语言实战》

Golang 入门 : goroutine(协程)的更多相关文章

  1. Golang的goroutine协程和channel通道

    一:简介 因为并发程序要考虑很多的细节,以保证对共享变量的正确访问,使得并发编程在很多情况下变得很复杂.但是Go语言在开发并发时,是比较简洁的.它通过channel来传递数据.数据竞争这个问题在gol ...

  2. golang中goroutine协程调度器设计策略

    goroutine与线程 /* goroutine与线程1. 可增长的栈os线程一般都有固定的栈内存,通常为2MB,一个goroutine的在其声明周期开始时只有很小的栈(2KB),goroutine ...

  3. golang中最大协程数的限制(线程)

    golang中最大协程数的限制 golang中有最大协程数的限制吗?如果有的话,是通过什么参数控制呢?还是通过每个协程占用的资源计算? 通过channel控制协程数的就忽略吧. 以我的理解,计算机资源 ...

  4. go语言之进阶篇创建goroutine协程

    1.goroutine是什么 goroutine是Go并行设计的核心.goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现 ...

  5. Golang教程:goroutine协程

    在上一篇中,我们讨论了并发,以及并发和并行的区别.在这篇教程中我们将讨论在Go中如何通过Go协程实现并发. 什么是协程 Go协程(Goroutine)是与其他函数或方法同时运行的函数或方法.可以认为G ...

  6. golang的多协程实践

    go语言以优异的并发特性而闻名,刚好手上有个小项目比较适合. 项目背景: 公司播控平台的数据存储包括MySQL和ElasticSearch(ES)两个部分,编辑.运营的数据首先保存在MySQL中,为了 ...

  7. Go goroutine (协程)

    在Go语言中goroutine是一个协程,但是跟Python里面的协程有很大的不同: 在任何函数前只需要加上go关键字就可以定义为协程; 不需要在定义时区分是否是异步函数  VS  async def ...

  8. go 学习 (五):goroutine 协程

    一.goroutine 基础 定义 使用者分配足够多的任务,系统能自动帮助使用者把任务分配到 CPU 上,让这些任务尽量并发运作,此机制在Go中称作 goroutine goroutine 是 Go语 ...

  9. golang:Channel协程间通信

    channel是Go语言中的一个核心数据类型,channel是一个数据类型,主要用来解决协程的同步问题以及协程之间数据共享(数据传递)的问题.在并发核心单元通过它就可以发送或者接收数据进行通讯,这在一 ...

随机推荐

  1. 手机端--tap PC端--click

    区别: tap为jq mobile 的方法 1.click与tap都会触发点击事件,但是在手机web端,click会有200-300ms的延迟,所以一般用tap代替click作为点击事件.single ...

  2. MySQL5

    MySQL数据库5 mysqldump备份恢复数据库 冷备份还原数据库 逻辑卷快照备份还原数据库 xtrabackup备份还原数据库 1. 备份和恢复概述 适用场景 硬件故障.软件故障.自然灾害.黑客 ...

  3. Python判断字符串是否全是字母或数字

    str.isnumeric(): True if 只包含数字:otherwise False.注意:此函数只能用于unicode string str.isdigit(): True if 只包含数字 ...

  4. Django-REST-Framework JWT 实现SSO认证(下)

    在上一篇博客中,我已经对JSON Web 认证做了简单的解释,这篇博客是续篇,若不了解,请看上一篇博客:https://www.cnblogs.com/yushenglin/p/10863184.ht ...

  5. Java核心技术 卷一 复习笔记(乙

    1.字符串从概念上讲,Java字符串就是Unicode字符序列.Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类,叫String. 每个用双引号括起来的字符串都是 String ...

  6. react入门-----(jsx语法,在react中获取真实的dom节点)

    1.jsx语法 var names = ['Alice', 'Emily', 'Kate']; <!-- HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的 ...

  7. hdu 2094拓扑排序map实现记录

    #include<stdio.h> #include<iostream> #include<algorithm> #include<string> #i ...

  8. 复习1背包dp

    背包问题是对于一个有限制的容器,一般计算可以装的物品的价值最值或数量.通常每个物品都有两个属性空间和价值,有时还有数量或别的限制条件,这个因体而异. 背包大概分成3部分,下面会细述这最经典的3种题型 ...

  9. 生成随机数验证码的工具类(from韩顺平)

    生成随机数验证码的工具类 package com.cx; //生成随机数的图片 import java.awt.Color; import java.awt.Font; import java.awt ...

  10. 武大OJ 622. Symmetrical

    Description          Cyy likes something symmetrical, and Han Move likes something circular. Han Mov ...