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

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

如果说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) // 启动协程任务后入队  
        go job(i)  
    }  
    wg.Wait() // 等待所有登记的goroutine都结束  
  
    fmt.Println("所有任务执行完毕")  
}  


程序返回:



协程任务执行 8  
协程任务执行 9  
协程任务执行 5  
协程任务执行 0  
协程任务执行 1  
协程任务执行 4  
协程任务执行 7  
协程任务执行 2  
协程任务执行 3  
协程任务执行 6  
所有任务执行完毕

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

和Python协程区别

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



import asyncio  
import random  
  
async def job(i):  
  
    print("协程任务执行{}".format(i))  
    await asyncio.sleep(random.randint(1,5))  
    print("协程任务结束{}".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())

程序返回:

协程任务执行0  
协程任务执行1  
协程任务执行2  
协程任务执行3  
协程任务执行4  
协程任务执行5  
协程任务执行6  
协程任务执行7  
协程任务执行8  
协程任务执行9  
协程任务结束0  
协程任务结束1  
协程任务结束3  
协程任务结束6  
协程任务结束9  
协程任务结束8  
协程任务结束2  
协程任务结束4  
协程任务结束5  
协程任务结束7

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

import asyncio  
import random  
  
async def job(i):  
  
    print("协程任务执行{}".format(i))  
    print("协程任务结束{}".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())

程序返回:

协程任务执行0  
协程任务结束0  
协程任务执行1  
协程任务结束1  
协程任务执行2  
协程任务结束2  
协程任务执行3  
协程任务结束3  
协程任务执行4  
协程任务结束4  
协程任务执行5  
协程任务结束5  
协程任务执行6  
协程任务结束6  
协程任务执行7  
协程任务结束7  
协程任务执行8  
协程任务结束8  
协程任务执行9  
协程任务结束9

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

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

结语

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

相关文章
|
2月前
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
109 33
|
1月前
|
数据采集 安全 Go
Go 语言并发编程基础:Goroutine 的创建与调度
Go 语言的 Goroutine 是轻量级线程,由 runtime 管理,具有启动快、占用小、支持高并发的特点。本章介绍 Goroutine 的基本概念、创建方式(如使用 `go` 关键字或匿名函数)、M:N 调度模型及其工作流程,并探讨其在高并发场景中的应用,帮助理解其高效并发的优势。
|
1月前
|
Go 开发者
Go 并发编程基础:无缓冲与有缓冲通道
本章深入探讨Go语言中通道(Channel)的两种类型:无缓冲通道与有缓冲通道。无缓冲通道要求发送和接收必须同步配对,适用于精确同步和信号通知;有缓冲通道通过内部队列实现异步通信,适合高吞吐量和生产者-消费者模型。文章通过示例对比两者的行为差异,并分析死锁风险及使用原则,帮助开发者根据场景选择合适的通道类型以实现高效并发编程。
|
3月前
|
存储 算法 数据可视化
【二叉树遍历入门:从中序遍历到层序与右视图】【LeetCode 热题100】94:二叉树的中序遍历、102:二叉树的层序遍历、199:二叉树的右视图(详细解析)(Go语言版)
本文详细解析了二叉树的三种经典遍历方式:中序遍历(94题)、层序遍历(102题)和右视图(199题)。通过递归与迭代实现中序遍历,深入理解深度优先搜索(DFS);借助队列完成层序遍历和右视图,掌握广度优先搜索(BFS)。文章对比DFS与BFS的思维方式,总结不同遍历的应用场景,为后续构造树结构奠定基础。
185 10
|
5月前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
125 3
Go 语言入门指南:切片
|
5月前
|
监控 Linux PHP
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
141 20
|
5月前
|
Go C语言
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
85 15
|
7月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
8月前
|
安全 Serverless Go
Go语言中的并发编程:深入理解与实践####
本文旨在为读者提供一个关于Go语言并发编程的全面指南。我们将从并发的基本概念讲起,逐步深入到Go语言特有的goroutine和channel机制,探讨它们如何简化多线程编程的复杂性。通过实例演示和代码分析,本文将揭示Go语言在处理并发任务时的优势,以及如何在实际项目中高效利用这些特性来提升性能和响应速度。无论你是Go语言的初学者还是有一定经验的开发者,本文都将为你提供有价值的见解和实用的技巧。 ####
|
8月前
|
算法 安全 程序员
Go语言的并发编程:深入理解与实践####
本文旨在探讨Go语言在并发编程方面的独特优势及其实现机制,通过实例解析关键概念如goroutine和channel,帮助开发者更高效地利用Go进行高性能软件开发。不同于传统的摘要概述,本文将以一个简短的故事开头,引出并发编程的重要性,随后详细阐述Go语言如何简化复杂并发任务的处理,最后通过实际案例展示其强大功能。 --- ###

推荐镜像

更多