教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang 深入浅出之 goroutine 理解

golang 深入浅出之 goroutine 理解

发布时间:2021-12-30   编辑:jiaochengji.com
教程集为您提供golang 深入浅出之 goroutine 理解等资源,欢迎您收藏本站,我们将为您提供最新的golang 深入浅出之 goroutine 理解资源

目录

一、前言

二、goroutine 的理解与使用

 (一)goroutine 入门

 (二)sync 包同步 goroutine

三、并发与并行

 (一)并发与并行的区别

 (二)runtime 包对 goroutine 控制

四、思考题

五、参考文献


<h1 id="一、前言">一、前言</h1>

       goroutine 是 Go 并行设计的核心,其本质就是协程,协程比线程小,也叫轻量级线程,它可以轻松创建上万个而不会导致系统资源的枯竭。十几个 goroutine 可能体现在底层就是五六个线程,一个线程可以有任意多个协程,但是某一个时刻只能有一个协程在运行,多个协程共享该线程分配到的计算机资源。创建 goroutine 只需在函数调用前加上 go 语句,就可以创建并发执行单元,开发人员无需了解任何执行细节,调度器会自动安排其到合适的系统线程上执行。

<h1 id="2. 如何使用 goroutine">二、goroutine 的理解与使用</h1> <h2 id=" (一)goroutine 入门"> (一)goroutine 入门</h2>

       首先如何创建一个 goroutine ,在调用的函数前加上 go 语句,该函数除了匿名函数也可以是 golang 包中自带的方法。

<pre><code class="language-Go">go func() { // to do something }() //比如: go fmt.Println(1) // 输出 1</code></pre>

        在并发的程序中,通常是将一个过程分为几块,然后让每个 goroutine 各自负责一块工作,当程序启动时,主函数在一个单独的 goroutine 中运行,我们叫他 <span style="color:#f33b45;">main goroutine</span> , 启动程序时 main goroutine 则立刻运行,其他 goroutine 会用 go 语句来创建。下面定义了 3 个函数,前两个函数加上 go 创建两个 goroutine,分别为 goroutine_1, goroutine_2, 第 3 个 func_1 为普通函数,作为对比用。

<pre><code class="language-Go">package main import ( "fmt" "time" ) func main() { go func() { fmt.Println("goroutine_1") }() go func() { fmt.Println("goroutine_2") }() func() { fmt.Println("func_1") }() }</code></pre>

       刚开始运行的时候只有 output1 这种输出,多运行几次之后出现了 output2 的输出。

       

       两个输出结果不一样,第一个输出结果 geroutine_1 , goroutine_2都没有输出,第二个输出也只是出现 goroutine_1。造成以上问题是因为 goroutine 的<span style="color:#f33b45;">运行时需要获得时间片</span>,获取 CPU 时间片才能运行 goroutine,在运行队列中等待调用,并不会马上执行, 在 main goroutine 中的 fun_1 则立刻运行。现在再看上面的程序,一共定义了两个 goroutine ,执行完两个定义语句之后,goroutine 不会马上运行,而是等待获取时间片执行,然而这段代码还没等到两个 goroutine 被调度执行 main goroutine 就结束运行并退出,main goroutine 退出后,其他的工作 goroutine 也会自动退出,所以就看不到两个 goroutine 的输出。为了看到两个 goroutine 的执行可以暂时在程序的末尾加上休眠时间。

<pre><code class="language-Go">package main import ( "fmt" "time" ) func main() { go func() { fmt.Println("goroutine_1") }() go func() { fmt.Println("goroutine_2") }() func() { fmt.Println("func_1") }() time.Sleep(time.Second) // 程序休眠1s }</code></pre>

       这时候就看到了两个 goroutine 输出结果。因为 goroutinue 的<span style="color:#f33b45;">调度执行是随机的</span>,所以每次运行的结果可能不一样,以下是两种输出的结果。fun_1因为是马上运行的,所以第一个输出。

     

       但是我们总不能每次在运行程序的时候都要休眠,毕竟我们不能自己算出所有的 goroutine 什么时候被调用执行,以及他们什么时候才能结束,所以 sync 包中的 WaitGroup 就可以用来判断 goroutine 是否运行结束的方法。

<h2 id=" (二)sync 包同步 goroutine"> (二)sync 包同步 goroutine</h2> <blockquote>

sync 包

sync.WaitGroup : 一个计数信号量,用来记录并维护 goroutine。

wg.Add(n) : 代表有 n 个正在运行的 goroutine。

wg.Done() :  其源码是 wg.Add(-1),说明有一个 goroutine 运行结束。

wg.Wait() :  如果 WaitGroup 的值大于 0 ,Wait 方法就会阻塞,直到等待队列的值到 0 并最终释放 main 函数。

</blockquote> <pre><code class="language-Go">package main import ( "fmt" "sync" ) func main() { // WaitGroup 是一个计数信号量,被 // 用来记录并维护 goroutine var wg sync.WaitGroup wg.Add(2) // 共有两个 goroutine go func(){ fmt.Println("goroutine 1") wg.Done() // goroutine 运行结束 }() go func(){ fmt.Println("goroutine 2") wg.Done() }() func() { fmt.Println("func_1") }() wg.Wait() // 阻塞等待所有 goroutine 运行完毕 //time.Sleep(time.Second) // 程序休眠1s }</code></pre>

 

        这样就可以保证所有的 goroutine 都被运行。 以下代码可以显示了添加 WaitGroup 阻塞判断和没有阻塞判断程序所耗时间。首先是没有加 sync.WaitGroup 的代码。

<pre><code class="language-Go">package main import ( "fmt" "time" ) func main() { startTime := time.Now().UnixNano() go func() { fmt.Println("goroutine_1") }() go func() { fmt.Println("goroutine_2") }() func() { fmt.Println("func_1") }() endTime := time.Now().UnixNano() nanoSeconds := float64((endTime - startTime)) fmt.Println(nanoSeconds) } </code></pre>

       输出结果为:

       在这 27000 纳秒内,main goroutine 程序就已经运行完毕,但是 goroutine_1, goroutine_2 没有被调用执行。然后我们将阻塞等待机制加入以上代码中,来对比两段程序的运行时间。

<pre><code class="language-Go">package main import ( "fmt" "sync" "time" ) func main() { startTime := time.Now().UnixNano() var wg sync.WaitGroup wg.Add(2) go func() { fmt.Println("goroutine_1") wg.Done() }() go func() { fmt.Println("goroutine_2") wg.Done() }() func() { fmt.Println("func_1") }() wg.Wait() endTime := time.Now().UnixNano() nanoSeconds := float64((endTime - startTime)) fmt.Println(nanoSeconds) } </code></pre>

       得到的输出为:

       这段程序运行了 goroutine_1, goroutine_2 ,一共花费了 65000 纳秒的时间。 很显然加了阻塞判断机制的程序为了等待 goroutine_1, goroutine_2 被调用执行,花费了更多的时间,当然这个时间并不是唯一的,goroutine 的调用时随机的,可能两个 goroutine 调用的晚,程序花费较多的时间,goroutine 调用得早,花费较少的时间,这个视系统的环境而定。

<h1 id="3. 并发与并行">三、并发与并行</h1> <h2 id=" (一)并发与并行的区别"> (一)并发与并行的区别</h2>

       Golang在运行时候会在逻辑处理器上调度 goroutine 来运行。每个逻辑处理器分别绑定绑定到单个操作系统线程。golang 运行时会默认为每个可用的物理处理器分配一个处理器,所以我们在运行代码的时候有时候会用  runtime.GOMAXPROCS(N) 来设置逻辑处理器的数量,其中 N 代表 N 个逻辑处理器,这些处理器会被用于执行所有的 goroutine。

       关于并发与并行的区别,知乎上很赞的回答,可以帮助理解两者的不同。<span style="color:#000000;">       </span>

<blockquote>

你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。

所以我认为它们最关键的点就是:是否是『同时』。

来源:知乎
 

</blockquote>

       并发是当正在运行的 goroutine 发生阻塞时,比如读取io,或者打开一个文件,通常都需要花费一定的时间等待。这类调用会让线程和 goroutine 从逻辑处理器分离,该线程会阻塞,并等待系统调用返回。调度器会重新创建一个新的线程,然后再继续从本地执行队列中选择一个 goroutine 运行。一旦被阻塞的 goroutine 执行完成并返回是,对应的 goroutine 会被放回本地运行队列,而之前的线程会被保存,以备之后继续使用,调度情况如图1.

                                                                               图1 Go 调度器如何管理 goroutine

       并发(concurrency)与并行(parallelism)是不同的。<span style="color:#f33b45;">并行是让不同的代码片段<em><u>同时</u></em>在不同的物理处理器上执行。并行的关键是同时做很多事情,并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情。</span>在很多情况下,并发比并行的效果好,因为操作系统的和硬件的总资源一般很少,但能支持系统做很多事情。这种"使用较少资源做更多事情”的哲学也是知道 Go 语言设计的哲学。图2 展示了并发与并行的区别。

<h2 id="3. 思考题"></h2>

                                                                               图2 并发与并行的区别

<h2 id=" (二)runtime 包对 goroutine 控制"> (二)runtime 包对 goroutine 控制</h2> <blockquote>

runtime 包

runtime.GOMAXPROCS(N)  设置 N 个逻辑处理器给调度器使用。当 N值为 runtime.NumCPU(),代表给每个可用的核心分配一个逻辑处理器。当 N 为 1,最多同时只能有一个 goroutine 被执行,当 N 为 2 时,两个 goroutine 可以被一起执行。

runtime.Gosched() 用于让出 CPU 时间片,让出当前 goroutine 的执行权限,调度器安排其他等待的任务先执行,并在下次再次获得 CPU 时间轮片的时候,从让出该 CPU 的位置恢复执行。

runtime.Goexit() 终止当前 goroutine 执行。

</blockquote>

       现在可以在刚才的代码上加上 go 语句,看看逻辑处理器的调度。先分配逻辑处理器,同时将两个 goroutine 方法改为输出 1 ~ 10。

<pre><code class="language-Go">// 输出 1 ~ n 的数字 func printNum(goroutine string, n int) { // 设置随机种子,随机种子不同,生成的随机数不同。 rand.Seed(time.Now().Unix()) for i := 1; i <= n; i { fmt.Printf("%s: %d\n", goroutine, i) // 程序随机休眠 0~1 秒,方便观察 goroutine 之间的切换 time.Sleep(time.Duration(rand.Intn(2)) * time.Second) } // 输出结束标志 fmt.Printf("%s: completed\n", goroutine) }</code></pre>

       输出 1~n 之间的数字,然后通过随机长度休眠模拟阻塞,<span style="color:#f33b45;">阻塞时间为 [0,n) 秒</span>,方便观察 goroutine 之间切换执行的并发效果。将以上代码整合到前面的程序中。

<pre><code class="language-Go">package main import ( "fmt" "math/rand" "runtime" "sync" "time" ) // 输出 1 ~ n 的数字 func printNum(goroutine string, n int) { // 设置随机种子,随机种子不同,生成的随机数不同。 rand.Seed(time.Now().Unix()) for i := 1; i <= n; i { fmt.Printf("%s: %d\n", goroutine, i) // 程序随机休眠 0~1 秒,方便观察 goroutine 之间的切换 time.Sleep(time.Duration(rand.Intn(2)) * time.Second) } // 输出结束标志 fmt.Printf("%s: completed\n", goroutine) } func main() { // WaitGroup 是一个计数信号量,被 // 用来记录并维护 goroutine runtime.GOMAXPROCS(1) var wg sync.WaitGroup wg.Add(2) // 共有两个 goroutine go func(){ printNum("1", 10) wg.Done() // goroutine 运行结束 }() go func(){ printNum("2", 10) wg.Done() }() func() { fmt.Println("func_1") }() wg.Wait() // 阻塞等待所有 goroutine 运行完毕 //time.Sleep(time.Second) // 程序休眠1s }</code></pre>

       运行程序后可以发现,当一个程序阻塞,调度器会切换到另外一个程序执行。以下为该程序的输出结果。

<pre><code class="language-Go">func_1 goroutine_2: 1 goroutine_1: 1 // 切换到 goroutine_1 goroutine_1: 2 goroutine_2: 2 // 切换到 goroutine_2 goroutine_2: 3 goroutine_2: 4 goroutine_2: 5 goroutine_2: 6 goroutine_2: 7 goroutine_2: 8 goroutine_2: 9 goroutine_1: 3 // 切换到 goroutine_1 goroutine_1: 4 goroutine_2: 10 // 切换到 goroutine_2 goroutine_2: completed goroutine_1: 5 // 切换到 goroutine_1 goroutine_1: 6 goroutine_1: 7 goroutine_1: 8 goroutine_1: 9 goroutine_1: 10 goroutine_1: completed Process finished with exit code 0 </code></pre>

       上图显示先是 goroutine_2 在运行输出,输出到数字 1 的时候,调度器将正在运行的 goroutine_2 转换为 goroutine_1,之后 goroutine_1 输出了数字 1~2,再次切换到 goroutine_2。通过这样的切换调度使得两个 goroutine 完成所有的工作。每次运行结果可能会不一样。如果将上面的休眠阻塞时间去掉,一般 goroutine 就会执行完所有的工作再切换另外一个。即 goroutine_1 输出完1~n,之后再切换 goroutine_2 输出 1~ n。对于 goroutine 的执行顺序,如果没有利用同步加以控制,那么 <span style="color:#f33b45;">goroutine 的执行顺序是无法预测的</span>。如果看到相同的执行顺序是因为测试的次数太少。

<h1 id="四. 思考题">四、思考题</h1>

        文章写到这里,我们对 goroutine 有了初步的了解,我在网上找了两个有趣的程序,用来加深理解。

        程序1:

<pre><code class="language-Go">package main import ( "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) for i := 0; i < 10; i { go func() { println(i) }() } time.Sleep(time.Second) } </code></pre>

       程序2:

<pre><code class="language-Go">package main import ( "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) for i := 0; i < 10; i { go println(i) } time.Sleep(time.Second) } </code></pre>

      请先思考一下两段程序的输出结果,相信理解了这两段程序之后可以对 goroutine 有更进一步的理解,输出结果在文末公布。

公布答案:

<pre><code class="language-Go">// 程序1 输出 10 个 10, 程序2 乱序输出 0~9 。为什么和程序1 结果不一样?</code></pre>

解析:

       首先程序1:我们在 for 循环里面定义了一个匿名函数,匿名函数的功能是输出 i 的值,结果输出 10 个 10。前面讲过了,除了 goroutine 会等待系统调用执行,其他代码会立刻运行,所以 for 循环会很快就结束,同时为了看到 goroutine 被调用执行,我们让 main 函数休眠 1 秒。当程序运行到 go 语句的时候,编译器就把运行 goroutine 所需的函数和参数都保存了,<span style="color:#f33b45;">程序1 中编译器保存的是{main.func_xxx, nil}</span>,第一个参数为函数,第二个参数为该函数接收的参数,此时应该注意到,该匿名函数是无参的,所以保存参数为 nil。当系统中的 goroutine 被调用执行时, 匿名函数里面的代码开始执行,此时需要用到 i 的值,然而 for 循环已经执行完毕,当前的 i 值为 10,所以后面输出的所有值都是10。

      其次是程序2:相比于程序1 唯一不同的地方就是 go 语句后面的内容,代码看起来没什么差别,但是结果却不同。经过程序1 的分析,我们知道运行到 go 语句时,编译器会保存方法和参数,<span style="color:#f33b45;">程序2 中编译器保存的是{println, current_i}</span> ,不同的是,这次编译器保存的方法为 println 且是有参数的,参数值为当前 i 的值,所以 for 循环执行了 10次,分别传入 i 的值为 0~9 ,因此当 goroutine 被调用执行的时候,输出的值为乱序的 0~9,之所以乱序,这是因为 goroutine 的调用时随机的,所以每次执行 0~9 序列都不尽相同。

<h1 id="五.参考文献">五、参考文献</h1>

[1]《 Go 语言实战》

[2] 深入浅出Golang关键字"go" 

[3] 并发与并行的区别?

[4] go语言之行--golang核武器goroutine调度原理、channel详解

到此这篇关于“golang 深入浅出之 goroutine 理解”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang 深入浅出之 goroutine 理解
简单理解 Goroutine 是如何工作的
Go:Goroutine 的切换过程实际上涉及了什么
golang runtime 简析
golang goroutine 通知_深入golang之---goroutine并发控制与通信
golang相关学习贴
Goroutine的调度分析(一)
Goroutine并发调度模型深度解析&amp;手撸一个协程池
golang并发原理剖析
goroutine 调度器

[关闭]
~ ~