并发与并行,同步和异步,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang并发编程之GoroutineEP13

如果说Go lang是静态语言中的皇冠,那么,Goroutine就是并发编程方式中的钻石。Goroutine是Go语言设计体系中最核心的精华,它非常轻量,一个 Goroutine 只占几 KB,并且这几 KB 就足够 Goroutine 运行完,这就能在有限的内存空间内支持大量 Goroutine协程任务,方寸之间,运筹帷幄,用极少的成本获取最高的效率,支持了更多的并发,毫无疑问,Goroutine是比Python的协程原理事件循环更高级的并发异步编程方式。

GMP调度模型(Goroutine-Machine-Processor)

为什么Goroutine比Python的事件循环高级?是因为Go lang的调度模型GMP可以参与系统内核线程中的调度,这里G为Goroutine,是被调度的最小单元;M是系统起了多少个线程;P为Processor,也就是CPU处理器,调度器的核心处理器,通常表示执行上下文,用于匹配 M 和 G 。P 的数量不能超过 GOMAXPROCS 配置数量,这个参数的默认值为当前电脑的总核心数,通常一个 P 可以与多个 M 对应,但同一时刻,这个 P 只能和其中一个 M 发生绑定关系;M 被创建之后需要自行在 P 的 free list 中找到 P 进行绑定,没有绑定 P 的 M,会进入阻塞状态,每一个P最多关联256个G。

说白了,就是GMP和Python一样,也是维护一个任务队列,只不过这个任务队列是通过Goroutine来调度,怎么调度?通过Goroutine和系统线程M的协商,寻找非阻塞的通道,进入P的本地小队列,然后交给系统内的CPU执行,藉此,充分利用了CPU的多核资源。

而Python的协程方式仅仅停留在用户态,它没法参与到线程内核的调度,弥补方式是单线程多协程任务下开多进程,Go lang则是全权交给Goroutine,用户不需要参与底层操作,同时又可以利用CPU的多核资源。

启动Goroutine

首先默认情况下,golang程序还是由上自下的串行方式:

package main

import (
    "fmt"
)

func job() {
    fmt.Println("任务执行")
}
func main() {
    job()
    fmt.Println("任务执行完了")
}

程序返回:

任务执行
任务执行完了

这里job中的打印函数是先于main中的打印函数。

现在,在执行job函数前面加上关键字go,也就是启动一个goroutine去执行job这个函数:

package main

import (
    "fmt"
    "time"
)

func job() {
    fmt.Println("任务执行")
}
func main() {
    go job()
    fmt.Println("任务执行完了")
    time.Sleep(time.Second)
}

注意,开启Goroutine是在函数执行的时候开启,并非声明的时候,程序返回:


任务执行完了
任务执行

可以看到,执行顺序颠倒了过来,首先为什么会先打印任务执行完了,是因为系统在创建新的Goroutine的时候需要耗费一些资源,因为就算只有几kb,也需要时间来创建,而此时main函数所在的goroutine是继续执行的。

第二,为什么要人为的把main函数延迟一秒钟?

因为当main()函数返回的时候main所在的Goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,所以这里必须人为的”阻塞”一下main函数,让它后于job结束,有点像公园如果要关门必须等最后一个游客走了才能关,否则就把游客关在公园里了,出不去了。

与此同时,此逻辑和Python中的线程阻塞逻辑非常一致,用过Python多线程的朋友肯定知道要想让所有子线程都执行完毕,必须阻塞主线程,不能让主线程提前执行完,这和Goroutine有异曲同工之妙。

在Go lang中实现并发编程就是如此轻松,我们还可以启动多个Goroutine:


package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func job(i int) {
    defer wg.Done() // 协程结束就通知
    fmt.Println("协程任务执行", i)
}
func main() {

    for i := 0; i < 10; i++ {
        wg.Add(1) // &#x542F;&#x52A8;&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x540E;&#x5165;&#x961F;
        go job(i)
    }
    wg.Wait() // &#x7B49;&#x5F85;&#x6240;&#x6709;&#x767B;&#x8BB0;&#x7684;goroutine&#x90FD;&#x7ED3;&#x675F;

    fmt.Println("&#x6240;&#x6709;&#x4EFB;&#x52A1;&#x6267;&#x884C;&#x5B8C;&#x6BD5;")
}

程序返回:


&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 8
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 9
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 5
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 0
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 1
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 4
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 7
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 2
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 3
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C; 6
&#x6240;&#x6709;&#x4EFB;&#x52A1;&#x6267;&#x884C;&#x5B8C;&#x6BD5;

这里我们摒弃了相对土鳖的time.Sleep(time.Second)方式,而是采用sync包的WaitGroup方式,原理是当启动协程任务后,在WaitGroup登记,当每个协程任务执行完成后,通知WaitGroup,直到所有的协程任务都执行完毕,然后再执行main函数所在的协程,所以”所有任务执行完毕”会在所有协程任务执行完毕后再打印。

和Python协程区别

我们再来看看,如果是Python,会怎么做?


import asyncio
import random

async def job(i):

    print("&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;{}".format(i))
    await asyncio.sleep(random.randint(1,5))
    print("&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;{}".format(i))

async def main():

    tasks = [asyncio.create_task(job(i)) for i in range(10)]

    res = await asyncio.gather(*tasks)

if __name__ == '__main__':
    asyncio.run(main())

程序返回:

&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;0
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;1
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;2
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;3
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;4
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;5
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;6
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;7
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;8
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;9
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;0
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;1
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;3
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;6
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;9
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;8
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;2
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;4
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;5
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;7

可以看到,Python协程工作的前提是,必须在同一个事件循环中,同时逻辑内必须由用户来手动切换,才能达到”并发”的工作方式,假设,如果我们不手动切换呢?

import asyncio
import random

async def job(i):

    print("&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;{}".format(i))
    print("&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;{}".format(i))

async def main():

    tasks = [asyncio.create_task(job(i)) for i in range(10)]

    res = await asyncio.gather(*tasks)

if __name__ == '__main__':
    asyncio.run(main())

程序返回:

&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;0
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;0
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;1
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;1
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;2
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;2
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;3
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;3
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;4
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;4
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;5
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;5
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;6
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;6
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;7
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;7
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;8
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;8
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x6267;&#x884C;9
&#x534F;&#x7A0B;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;9

一望而知,只要你不手动切任务,它就立刻回到了”串行”的工作方式,同步的执行任务,那么协程的意义在哪儿呢?

所以,归根结底,Goroutine除了可以极大的利用系统多核资源,它还能帮助开发者来切换协程任务,简化开发者的工作,说白了就是,不懂协程工作原理,也能照猫画虎写go lang代码,但如果不懂协程工作原理的前提下,写Python协程并发逻辑呢?恐怕够呛吧。

综上,Goroutine的工作方式,就是多个协程在多个线程上切换,既可以用到多核,又可以减少切换开销。但有光就有影,有利就有弊,Goroutine确实不需要开发者过度参与,但这样开发者就少了很多自由度,一些定制化场景下,就只能采用单一的Goroutine手段,比如一些纯IO密集型任务场景,像爬虫,你有多少cpu的意义并不大,因为cpu老是等着你的io操作,所以Python这种协程工作方式在纯IO密集型任务场景下并不逊色于Goroutine。

Original: https://www.cnblogs.com/v3ucn/p/16640023.html
Author: 刘悦的技术博客
Title: 并发与并行,同步和异步,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang并发编程之GoroutineEP13

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/516205/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球