首頁  >  文章  >  後端開發  >  聊聊Go的並發程式設計 (二)

聊聊Go的並發程式設計 (二)

咔咔
咔咔原創
2021-07-07 16:16:551522瀏覽

聊聊Go的goroutine和Channel

    • 一、使用channel等待任務結束
      • sync.WaitGroup的用法
      • 抽象程式碼
    • 二、使用select進行調度
      • #計時器的使用
      ##三、總結

    相關文章推薦:《聊聊Go的並發程式設計(一)

    一、使用channel等待任務結束

    使用案例還是在第一篇的第二節中寫的程式碼,不過這裡只需要一段即可。

    package mainimport (
    	"fmt"
    	"time")func createWorker(id int) chan<div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb></div><div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb>這裡咔咔將原始原始碼放在這裡,如果你想跟著文章的節奏走,可以放到你的編輯器中進行操作。 <p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;"></p>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">那這段程式碼的問題在哪裡呢? </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">可以看到channelDemo函數最後使用了一個sleep,這玩意在程式中可不能亂用。 </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">說到這裡跟大家講一個小故事,咔咔之前在網路上看到一段就是加了sleep的程式碼。 </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">然後一個新手程式設計師不明白為什麼要加這個sleep,然後問題專案經理,專案經理說老闆發現程式慢之後會找咱們優化,每一次優化把這個sleep的時間縮短即可。讓老闆感覺到我們在做事情。 </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">新手就是新手對不懂程式碼都會進行標註,然後就寫了一句註解「專案經理要求這裡運行緩慢,老闆讓優化時,程式碼得到明顯的速度提升」。 </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">這句話很不巧的是被老闆給看見了,老闆不認識程式碼,但文字還是認識的哈!於是,專案經理下馬。 </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">所以說對於sleep大多數都是一個測試狀態,堅決不會出現在線上的,所以呢?就要解決程式碼中的這個sleep。 </p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">那么大家在回忆一下,在这里为什么要加sleep呢?</p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">发送到channel的数据都是在另一个goroutine中进行并发打印的,并发打印就会出现问题,因为根本不会知道什么时候才打印完毕。</p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">所以说这个sleep就会为了应对这个不知道什么时候打印完的问题,给个1毫秒让进行打印。</p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">这种做法是非常不好的,接下来看看使用一种新的方式来解决这个问题。</p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><p style="box-sizing: border-box; margin-top: 0px; margin-bottom: 16px; font-size: 18px; color: rgb(77, 77, 77); line-height: 26px; overflow: auto hidden;">以下代码是修改完的代码。</p></div>
    <div class="cl-preview-section" style="box-sizing: border-box; color: rgba(0, 0, 0, 0.75); font-family: -apple-system, " sf ui text arial sc sans gb yahei micro hei sans-serif font-size: font-variant-ligatures: common-ligatures white-space: normal background-color: rgb><pre style="box-sizing: border-box; font-family: " source code pro sans mono menlo monaco consolas inconsolata courier monospace sc yahei sans-serif font-size: margin-top: margin-bottom: line-height: color: rgb>package mainimport (
    	"fmt")type worker struct {
    	in   chan int
    	done chan bool}func createWorker(id int) worker {
    	w := worker{
    		in:   make(chan int),
    		done: make(chan bool),
    	}
    	go doWorker(id, w.in, w.done)
    	return w}func doWorker(id int, c chan int, done chan bool) {
    	for n := range c {
    		fmt.Printf("Worker %d receive %c\n", id, n)
    		done 

    将这些代码复制到你的本地,然后再来看一下都做了什么改动。

    • 首先為了參數傳遞方便,建立了一個結構體worker
    • 並且把之前的worker方法改為了doWorker
    • 這時候createWorker方法回傳值就不能是之前的channel了,而是創建的結構體worker
    • 然後在createWorker方法裡邊把channel全部創建好。並且使用結構體給doWorker傳遞參數。
    • 最終回傳的就是結構體。
    • 最後一步就是給channelDemo方法裡邊發送資料的兩個循環裡邊接收一下workers[i]的值即可。

    看看列印結果

    聊聊Go的並發程式設計 (二)

    #是不是有點懵,這怎麼成有序的了,如果是並行的那還有必要開那10個worker,直接按照順序打印就好了。
    #########現在就來解決這個問題,我不希望發一個任務然後等它結束。 ############最好的就是把他們全部發出去,等待它們全部結束再退出來。 ######

    代码实现如下

    package mainimport (
    	"fmt")type worker struct {
    	in   chan int
    	done chan bool}func createWorker(id int) worker {
    	w := worker{
    		in:   make(chan int),
    		done: make(chan bool),
    	}
    	go doWorker(id, w.in, w.done)
    	return w}func doWorker(id int, c chan int, done chan bool) {
    	for n := range c {
    		fmt.Printf("Worker %d receive %c\n", id, n)
    		done 

    在这里再进行打印看一下结果,你会发现代码是有问题的。

    聊聊Go的並發程式設計 (二)

    为什么将小写的字母打印出来,而打印大写字母时发生了报错呢?

    这个就要追溯到代码中了,因为我们代码本身就写的有问题。

    还是回归到本文长谈的一个问题,那就是对于所有的channel有发送数据就必须有接收数据,如果没有接收数据就会报错。

    那麼在程式碼中你能看出是那塊只進行了發送數據,而沒有接收數據嗎?

    聊聊Go的並發程式設計 (二)

    這個問題就是當給channel把小寫字母發送了後,就會到進入到doWorker方法,然後給done發送了一個true,但是接收done的方法是在後面,也就是說第二個發送大寫字母時,就會發送循環的等待。

    解決這個問題也很簡單,我們只需要並發的發送done。

    聊聊Go的並發程式設計 (二)

    看到列印結果也是正確的。

    本文給的這個案例在一般專案中是不會出現的,所以說不用糾結於此。

    給的案例就是為了讓大家更熟悉channel的機製而已。

    對於這個解決方法還有一個方案解決,請看程式碼。

    聊聊Go的並發程式設計 (二)

    將程式碼還原到之前,然後在每一個發送字母的下面循環接收done即可。

    對於這種多任務等待方式在go中有一個函式庫是可以來做這個事情,接下來看一下。

    sync.WaitGroup的用法

    對於sync.WaitGroup的用法喀喀就不一一介紹了,簡單的看一下原始碼的實作即可。

    package mainimport (
    	"fmt"
    	"sync")type worker struct {
    	in chan int
    	wg *sync.WaitGroup}func createWorker(id int, wg *sync.WaitGroup) worker {
    	w := worker{
    		in: make(chan int),
    		wg: wg,
    	}
    	go doWorker(id, w.in, wg)
    	return w}func doWorker(id int, c chan int, wg *sync.WaitGroup) {
    	for n := range c {
    		fmt.Printf("Worker %d receive %c\n", id, n)
    		wg.Done()
    	}}func channelDemo() {
    	var wg sync.WaitGroup	var workers [10]worker	for i := 0; i 

    这份源码也是非常简单的,具体修改得东西咔咔简单介绍一下。

    • 首先取消了channelDemo这个方法中关于done的channel。
    • 使用了sync.WaitGroup,并且给createWorker方法传递sync.WaitGroup
    • createWorker方法使用了 worker的结构体。
    • 所以要先修改worker结构体,将之前的done改为wg *sync.WaitGroup即可
    • 这样就可以直接用结构体的数据。
    • 接着在doWorker方法中把最后一个参数done改为wg *sync.WaitGroup
    • 将方法中的done改为wg.Done()
    • 最后一步就是回到函数channelDemo中把任务数添加进去,然后在代码最后添加一个等待即可。

    关于这块的内容先知道这么用即可,咔咔后期会慢慢的补充并且深入。

    抽象代码

    这块的代码看起来不是那么的完美的,接下来抽象一下。

    聊聊Go的並發程式設計 (二)

    这块代码有没有发现有点蹩脚,接下来我们使用函数式编程进行简单的处理。

    package mainimport (
    	"fmt"
    	"sync")type worker struct {
    	in   chan int
    	done func()}func createWorker(id int, wg *sync.WaitGroup) worker {
    	w := worker{
    		in: make(chan int),
    		done: func() {
    			wg.Done()
    		},
    	}
    	go doWorker(id, w)
    	return w}func doWorker(id int, w worker) {
    	for n := range w.in {
    		fmt.Printf("Worker %d receive %c\n", id, n)
    		w.done()
    	}}func channelDemo() {
    	var wg sync.WaitGroup	var workers [10]worker	for i := 0; i 

    这块代码看不明白就先放着,写的时间长了,你就会明白其中的含义了,学习东西不要钻牛角尖。

    二、使用select进行调度

    开头先给一个问题,假设现在有俩个channel,谁来的快先收谁应该怎么做?

    package mainimport (
    	"fmt"
    	"math/rand"
    	"time")func generator() chan int {
    	out := make(chan int)
    	go func() {
    		i := 0
    		for {
    			// 随机睡眠1500毫秒以内
    			time.Sleep(
    				time.Duration(rand.Intn(1500)) *
    					time.Millisecond)
    			// 往out这个channel发送i值
    			out 

    以上就是代码实现,代码注释也写的非常的清晰明了,就不过多的做解释了。

    主要用法还是对channel的使用,在带上了一个新的概念select,可以在多个通道,那个通道先发送数据,就先执行谁,并且这个select也是可以并行执行channel管道。

    在上文写的createWorkerworker俩个方法还记得吧!接下来就不在select里边直接打印了。

    就使用之前写的俩个方法融合在一起,咔咔已将将源码写好了,接下来看一下实现。

    package mainimport (
    	"fmt"
    	"math/rand"
    	"time")func worker(id int, c chan int) {
    	for n := range c {
    		fmt.Printf("Worker %d receive %d\n", id, n)
    	}}func createWorker(id int) chan

    运行代码

    聊聊Go的並發程式設計 (二)

    看到聊聊Go的並發程式設計 (二)得知也是没有问题的。

    这段代码虽然运行没有任何问题,但是这样有什么缺点呢?

    聊聊Go的並發程式設計 (二)

    可以看下这段代码n := 这里先收了一个值,然后在下边代码<code style="box-sizing: border-box; font-family: " source code pro sans mono menlo monaco consolas inconsolata courier monospace sc yahei sans-serif font-size: background-color: rgb border-radius: padding: line-height: color:>w 又会阻塞住,这个是不好的。

    那么希望是怎么执行的呢?

    聊聊Go的並發程式設計 (二)

    这种模式是在select中既可以收数据,也可以发数据,目前这个程序是编译不过的,请看修改后的源码。

    package mainimport (
    	"fmt"
    	"math/rand"
    	"time")func worker(id int, c chan int) {
    	for n := range c {
    		fmt.Printf("Worker %d receive %d\n", id, n)
    	}}func createWorker(id int) chan

    这个模式还是有缺点的,因为n收c1和c2的速度跟消耗的速度是不一样的。

    假设c1的生成速度特别快,一下子生成了1,2,3。那么最后输出的数据有可能就只有3,而1和2就无法输出了。

    这个场景也是非常好模拟的,只需要在打印的位置加上一点延迟时间即可。

    聊聊Go的並發程式設計 (二)

    此时你会看到聊聊Go的並發程式設計 (二)为0、7、12、20…中间很多的数字都没来得急打印。

    因此我们就需要把收到的n存下来进行排队输出。

    package mainimport (
    	"fmt"
    	"math/rand"
    	"time")func worker(id int, c chan int) {
    	for n := range c {
    		// 手动让消耗速度变慢
    		time.Sleep(5 * time.Second)
    		fmt.Printf("Worker %d receive %d\n", id, n)
    	}}func createWorker(id int) chan 0 {
    			activeWorker = worker			// 取出索引为0的值
    			activeValue = values[0]
    		}
    		/**
    		select 方式进行调度
    		        使用场景:比如有多个通道,但我打算是哪一个通道先给我数据,我就先执行谁
    		        这个select 可以是并行执行 channel管道
    		*/
    		select {
    		case n := 

    以上就是实现代码

    此时在来看聊聊Go的並發程式設計 (二)。

    聊聊Go的並發程式設計 (二)

    聊聊Go的並發程式設計 (二)没有漏掉数据,并且也是无序的,这样就非常好了。

    计时器的使用

    上面的这个程序是退出不了的,我们想让它10s后就直接退出怎么做呢?

    那就需要使用计时器来进行操作了。

    package mainimport (
    	"fmt"
    	"math/rand"
    	"time")func worker(id int, c chan int) {
    	for n := range c {
    		// 手动让消耗速度变慢
    		time.Sleep(time.Second)
    		fmt.Printf("Worker %d receive %d\n", id, n)
    	}}func createWorker(id int) chan 0 {
    			activeWorker = worker			// 取出索引为0的值
    			activeValue = values[0]
    		}
    		/**
    		select 方式进行调度
    		        使用场景:比如有多个通道,但我打算是哪一个通道先给我数据,我就先执行谁
    		        这个select 可以是并行执行 channel管道
    		*/
    		select {
    		case n := 

    这里就是源码的实现,可以看到直接在select中是可以收到tm的值的,也就说如果到了10s,就会执行打印bye的操作。

    那麼現在還有另一個需求,就是如果在800毫秒的時間內還沒有收到數據,可以做其它事情。

    使用舉一反三的思想,你可以思考這件事情該怎麼做。

    聊聊Go的並發程式設計 (二)

    其實也就很簡單了,只需要在case中在設定一個計時器即可。

    既然說到了這裡就在給大家補充一個用法聊聊Go的並發程式設計 (二) := time.Tick(time.Second)

    同樣也是在case中使用。

    聊聊Go的並發程式設計 (二)

    这样就可以每秒来显示一下values队列有多少数据。

    这块的内容就结束了,最终给大家发一下源码,感兴趣的可以在自己的编辑器上试试看。

    package mainimport (
    	"fmt"
    	"math/rand"
    	"time")func worker(id int, c chan int) {
    	for n := range c {
    		// 手动让消耗速度变慢
    		time.Sleep(time.Second)
    		fmt.Printf("Worker %d receive %d\n", id, n)
    	}}func createWorker(id int) chan 0 {
    			activeWorker = worker			// 取出索引为0的值
    			activeValue = values[0]
    		}
    		/**
    		select 方式进行调度
    		        使用场景:比如有多个通道,但我打算是哪一个通道先给我数据,我就先执行谁
    		        这个select 可以是并行执行 channel管道
    		*/
    		select {
    		case n := 

    三、总结

    本文主要就是对于goroutine和channel的大量练习。

    文中的案例,有可能会一时半会理解不了,是没有关系的,不用钻牛角尖的。

    等你在go的海洋裡遨遊的時間長了,有些東西就自然而然的明白了。

    下一期的文章就是給大家實戰一個go的並發爬蟲計畫。

    堅持學習、堅持寫作、堅持分享是咔咔從業以來一直所秉持的信念。希望在偌大互聯網中咔咔的文章能帶給你一絲絲幫助。我是喀喀,下期見。

    #

    以上是聊聊Go的並發程式設計 (二)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    select 结构体 循环 并发 channel
    陳述:
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
    上一篇:聊聊Go的並發程式設計 (一)下一篇:聊聊Go的並發程式設計 (一)

    相關文章

    看更多