zoukankan      html  css  js  c++  java
  • Golang 入门 : goroutine(协程)

    在操作系统中,执行体是个抽象的概念。与之对应的实体有进程、线程以及协程(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 := 0; i < 3; i++ {
            time.Sleep(100 * time.Millisecond)
            fmt.Println(s)
        }
    }
    
    func main() {
        go say("hello world")
        time.Sleep(1000 * 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(1)
    
        // wg用来等待程序完成
        // 计数加2,表示要等待两个goroutine
        var wg sync.WaitGroup
        wg.Add(2)
    
        fmt.Println("Start Goroutines")
    
        // 声明一个匿名函数,并创建一个goroutine
        go func(){
            // 在函数退出时调用Done来通知main函数工作已经完成
            defer wg.Done()
    
            // 显示字母表3次
            for count := 0; count< 3; count++{
                for char := 'a'; char< 'a'+26; char++{
                    fmt.Printf("%c ", char)
                }
                fmt.Println()
            }
        }()
        // 声明一个匿名函数,并创建一个goroutine
        go func(){
            // 在函数退出时调用Done来通知main函数工作已经完成
            defer wg.Done()
    
            // 显示字母表3次
            for count := 0; count< 3; count++{
                for char := 'A'; char< 'A'+26; 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(1)
    
        // 计数加2,表示要等待两个goroutine
        wg.Add(2)
    
        // 创建两个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 := 2; outer < 5000; outer++ {
            for inner := 2; inner < outer; inner++ {
                if outer % inner == 0 {
                    continue next
                }
            }
            fmt.Printf("%s:%d
    ", prefix, outer)
        }
        fmt.Println("Completed", prefix)
    }

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

    Create Goroutines
    Waiting To Finish
    B:2
    B:3
    ...
    B:3851          
    A:2             ** 切换 goroutine
    A:3
    ...
    A:4297          
    B:3853          ** 切换 goroutine
    ...
    B:4999
    Completed B
    A:4327          ** 切换 goroutine
    ...
    A:4999
    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(2)

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

    ...
    B:1741
    B:1747
    A:241
    A:251
    B:1753
    A:257
    A:263
    A:269
    A:271
    A:277
    B:1759
    A:281
    ...

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

    参考:
    《Go语言实战》

  • 相关阅读:
    Tp控制器
    thinkphp总体设计
    视频会议管理系统操作步骤(新)
    视频会议(旧)
    HCIE实验LAB_1(1)
    HCIE实验LAB_1(2)
    HCIE实验LAB_1(3)
    HCIE实验LAB_1(4)
    HCIE实验LAB_1(5)
    HCIE实验LAB_1(6)
  • 原文地址:https://www.cnblogs.com/sparkdev/p/10930168.html
Copyright © 2011-2022 走看看