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

goroutine

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

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

入门 demo

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

package main

import (
"time"
"fmt"
) func say(s string) {
for i := ; i < ; i++ {
time.Sleep( * time.Millisecond)
fmt.Println(s)
}
} func main() {
go say("hello world")
time.Sleep( * time.Millisecond)
fmt.Println("over!")
}

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

hello world
hello world
hello world
over!

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

goroutine 的生命周期

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

package main

import (
"runtime"
"sync"
"fmt"
) func main() {
// 分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS() // wg用来等待程序完成
// 计数加2,表示要等待两个goroutine
var wg sync.WaitGroup
wg.Add() fmt.Println("Start Goroutines") // 声明一个匿名函数,并创建一个goroutine
go func(){
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done() // 显示字母表3次
for count := ; count< ; count++{
for char := 'a'; char< 'a'+; char++{
fmt.Printf("%c ", char)
}
fmt.Println()
}
}()
// 声明一个匿名函数,并创建一个goroutine
go func(){
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done() // 显示字母表3次
for count := ; count< ; count++{
for char := 'A'; char< 'A'+; char++{
fmt.Printf("%c ", char)
}
fmt.Println()
}
}() // 等待goroutine结束
fmt.Println("Waiting To Finish")
wg.Wait() fmt.Println("Terminating Program")
}

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

Start Goroutines
Waiting To Finish
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
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
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
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
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
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
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 继续之前的工作。

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

package main

import (
"runtime"
"sync"
"fmt"
) func main() {
// wg用来等待程序完成
var wg sync.WaitGroup // 分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS() // 计数加2,表示要等待两个goroutine
wg.Add() // 创建两个goroutine
fmt.Println("Create Goroutines")
go printPrime("A", &wg)
go printPrime("B", &wg) // 等待goroutine结束
fmt.Println("Waiting To Finish")
wg.Wait() fmt.Println("Terminating Program")
} // printPrime 显示5000以内的素数值
func printPrime(prefix string, wg *sync.WaitGroup){
// 在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done() next:
for outer := ; outer < ; outer++ {
for inner := ; inner < outer; inner++ {
if outer % inner == {
continue next
}
}
fmt.Printf("%s:%d\n", prefix, outer)
}
fmt.Println("Completed", prefix)
}

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

Create Goroutines
Waiting To Finish
B:
B:
...
B:
A: ** 切换 goroutine
A:
...
A:
B: ** 切换 goroutine
...
B:
Completed B
A: ** 切换 goroutine
...
A:
Completed A
Terminating Program

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

让 goroutine 并行执行

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

runtime.GOMAXPROCS(runtime.NumCPU())

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

runtime.GOMAXPROCS()

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

...
B:
B:
A:
A:
B:
A:
A:
A:
A:
A:
B:
A:
...

除了这个 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. Linux 安装 MySQL 详解(rpm 包)

    说明:Linux 系统中软件的安装在 root 用户下进行,此安装方式为 rpm 包方式,安装的版本为:MySQL-5.6.25-1.linux_glibc2.5.x86_64.rpm-bundle. ...

  2. Extract local angle of attack on wind turbine blades

    Extract local angle of attack on wind turbine blades Table of Contents 1. Extract local angle of att ...

  3. 1.Zigbee开发学习资源

    http://blog.csdn.net/zhanglianpin/article/details/46907349

  4. python之MD5、base64\base32解密

    # -*- coding:utf-8 -*- import hashlib import base64 # 求最大公约数gys # def gys(m, n): # c = 1 # while(c ! ...

  5. Linux - 模块编程初试

    计算机网络的课程设计要做防火墙,老师没有限制在什么系统上面做,所以决定在Linux上实现.找了一下相关的资料,发现其实Linux有提供Netfilter/Iptables,为用户提供防火墙的功能,稍微 ...

  6. python——re模块(正则表达式)

    re 模块的使用: 1.使用compile()函数编译一个parttern对象, 例如:parttern=re.compile(r'\d+') 2.通过pattern对象提供的一系列属相和方法,对文本 ...

  7. Choose and divide

    The binomial coefficient C(m, n) is defined as C(m, n) = m! (m − n)! n! Given four natural numbers p ...

  8. poj——1330 Nearest Common Ancestors

    Nearest Common Ancestors Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 30082   Accept ...

  9. 重啓ubuntu后 VNC 自動運行

    Vino-Server是Ubuntu自带的有個缺点:重启后不能自動運行(可能是基於安全吧!) 親身測試对象:windows & ubuntu 10.04已安装图形桌面gnome ***wind ...

  10. POJ 1260-Pearls(DP)

    Pearls Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 7465   Accepted: 3695 Descriptio ...