欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go并發(fā)編程中使用channel的方法

 更新時間:2021年11月23日 09:29:02   作者:深度思維者  
本文給大家介紹Go并發(fā)編程中使用channel的方法,通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧

一.設計原理

Go 語言中最常見的、也是經(jīng)常被人提及的設計模式就是:

"不要通過共享內(nèi)存來通信,我們應該使用通信來共享內(nèi)存"

通過共享內(nèi)存來通信是直接讀取內(nèi)存的數(shù)據(jù),而通過通信來共享內(nèi)存,是通過發(fā)送消息的方式來進行同步。

而通過發(fā)送消息來同步的這種方式常見的就是 Go 采用的通信順序進程 CSP(Communication Sequential Process) 模型以及 Erlang 采用的 Actor 模型,這兩種方式都是通過通信來共享內(nèi)存。

如下圖所示

大部分的語言采用的都是第一種方式直接去操作內(nèi)存,然后通過互斥鎖,CAS 等操作來保證并發(fā)安全。Go 引入了 Channel 和 Goroutine 實現(xiàn) CSP 模型來解耦這個操作。

優(yōu)點:

在 Goroutine 當中我們就不用手動去做資源的鎖定與釋放,同時將生產(chǎn)者和消費者進行了解耦,Channel 其實和消息隊列很相似。

缺點:

由于 Channel 底層也是通過這些低級的同步原語實現(xiàn)的,所以性能上會差一些,如果有極高的性能要求時也可以用 sync 包中提供的低級同步原語

先入先出

目前的 Channel 收發(fā)操作均遵循了先進先出的設計,具體規(guī)則如下:

  • 先從 Channel 讀取數(shù)據(jù)的 Goroutine 會先接收到數(shù)據(jù);
  • 先向 Channel 發(fā)送數(shù)據(jù)的 Goroutine 會得到先發(fā)送數(shù)據(jù)的權(quán)利;

無鎖管道

鎖(Lock) 是一種常見的并發(fā)控制技術(shù),我們一般會將鎖分成樂觀鎖 和 悲觀鎖,即樂觀并發(fā)控制和悲觀并發(fā)控制,無鎖(lock-free)隊列更準確的描述是使用樂觀并發(fā)控制的隊列。樂觀并發(fā)控制也叫樂觀鎖,很多人都會誤以為樂觀鎖是與悲觀鎖差不多,然而它并不是真正的鎖,只是一種并發(fā)控制的思想.

樂觀并發(fā)控制本質(zhì)上是基于驗證的協(xié)議,我們使用原子指令 CAS(compare-and-swap 或者 compare-and-set)在多線程中同步數(shù)據(jù),無鎖隊列的實現(xiàn)也依賴這一原子指令。

從某種程度上說,Channel 是一個用于同步和通信的有鎖隊列,使用互斥鎖解決程序中可能存在的線程競爭問題

Go 語言社區(qū)也在 2014 年提出了無鎖 Channel 的實現(xiàn)方案,該方案將 Channel 分成了以下三種類型:

同步 Channel — 無緩沖區(qū),發(fā)送方會直接將數(shù)據(jù)交給(Handoff)接收方

異步channel: 基于環(huán)形緩存的傳統(tǒng)生產(chǎn)者消費者模型;

chan struct{} 類型的異步 Channel — struct{} 類型不占用內(nèi)存空間,不需要實現(xiàn)緩沖區(qū)和直接發(fā)送(Handoff)的語義;

二.數(shù)據(jù)結(jié)構(gòu)

Go 語言的 Channel 在運行時使用 runtime.hchan 結(jié)構(gòu)體表示。我們在 Go 語言中創(chuàng)建新的 Channel 時,實際上創(chuàng)建的都是如下所示的結(jié)構(gòu):

type hchan struct {
	qcount   uint           // 隊列中元素總數(shù)量
	dataqsiz uint           // 循環(huán)隊列的長度
	buf      unsafe.Pointer // 指向長度為 dataqsiz 的底層數(shù)組,只有在有緩沖時這個才有意義
	elemsize uint16         // 能夠發(fā)送和接受的元素大小
	closed   uint32         // 是否關(guān)閉
	elemtype *_type         // 元素的類型
	sendx    uint           // 當前已發(fā)送的元素在隊列當中的索引位置
	recvx    uint           // 當前已接收的元素在隊列當中的索引位置
	recvq    waitq          // 接收 Goroutine 鏈表
	sendq    waitq          // 發(fā)送 Goroutine 鏈表
 
	lock mutex              // 互斥鎖
}
 
// waitq 是一個雙向鏈表,里面保存了 goroutine
type waitq struct {
	first *sudog
	last  *sudog
}

如下圖所示,channel 底層其實是一個循環(huán)隊列

三.創(chuàng)建管道

Go 語言中所有 Channel 的創(chuàng)建都會使用 make 關(guān)鍵字。創(chuàng)建的表達式使用 make(chan T, cap) 來創(chuàng)建 channel.

如果不向 make 傳遞表示緩沖區(qū)大小的參數(shù),那么就會設置一個默認值 0,也就是當前的 Channel 不存在緩沖區(qū)。

四. 發(fā)送數(shù)據(jù)

當想要向 Channel 發(fā)送數(shù)據(jù)時,就需要使用 ch <- i 語句.

在發(fā)送數(shù)據(jù)的邏輯執(zhí)行之前會先為當前 Channel 加鎖,防止多個線程并發(fā)修改數(shù)據(jù)。

如果 Channel 已經(jīng)關(guān)閉,那么向該 Channel 發(fā)送數(shù)據(jù)時會報 “send on closed channel” 錯誤并中止程序。

4.1 直接發(fā)送

如果 Channel 沒有被關(guān)閉并且已經(jīng)有處于讀等待的 Goroutine,會取出最先陷入等待的 Goroutine 并直接向它發(fā)送數(shù)據(jù):

直接發(fā)送的過程稱為兩個部分:

  • 調(diào)用 runtime.sendDirect將發(fā)送的數(shù)據(jù)直接拷貝到 x = <-c 表達式中變量 x 所在的內(nèi)存地址上;
  • 調(diào)用 runtime.goready 將等待接收數(shù)據(jù)的 Goroutine 標記成可運行狀態(tài) Grunnable 并把該 Goroutine 放到發(fā)送方所在的處理器的 runnext 上等待執(zhí)行,該處理器在下一次調(diào)度時會立刻喚醒數(shù)據(jù)的接收方;

需要注意的是,發(fā)送數(shù)據(jù)的過程只是將接收方的 Goroutine 放到了處理器的 runnext 中,程序沒有立刻執(zhí)行該 Goroutine。

4.2 緩沖區(qū)

如果創(chuàng)建的 Channel 包含緩沖區(qū)并且 Channel 中的數(shù)據(jù)沒有裝滿,會使用 runtime.chanbuf 計算出下一個可以存儲數(shù)據(jù)的位置,然后通過 runtime.typedmemmove 將發(fā)送的數(shù)據(jù)拷貝到緩沖區(qū)中并增加 sendx 索引和 qcount 計數(shù)器。

4.3 阻塞發(fā)送

當 Channel 沒有接收者能夠處理數(shù)據(jù)時,向 Channel 發(fā)送數(shù)據(jù)會被下游阻塞,當然使用 select 關(guān)鍵字可以向 Channel 非阻塞地發(fā)送消息。

4.4 小結(jié)

可以簡單梳理和總結(jié)一下使用 ch <- i 表達式向 Channel 發(fā)送數(shù)據(jù)時遇到的幾種情況:

  • 如果當前 Channel 的 recvq 上存在已經(jīng)被阻塞的 Goroutine,那么會直接將數(shù)據(jù)發(fā)送給當前 Goroutine 并將其設置成下一個運行的 Goroutine;
  • 如果 Channel 存在緩沖區(qū)并且其中還有空閑的容量,我們會直接將數(shù)據(jù)存儲到緩沖區(qū) sendx 所在的位置上;
  • 如果不滿足上面的兩種情況,當前 Goroutine 也會陷入阻塞等待其他的協(xié)程從 Channel 接收數(shù)據(jù);

五. 接收數(shù)據(jù)

可以使用兩種不同的方式去接收 Channel 中的數(shù)據(jù):

i <- ch

i, ok <- ch

5.1 直接接收

會根據(jù)緩沖區(qū)的大小分別處理不同的情況

如果 Channel 不存在緩沖區(qū),直接從發(fā)送者那里把數(shù)據(jù)拷貝給接收變量如果是有緩沖 channel將隊列中的數(shù)據(jù)拷貝到接收方的內(nèi)存地址;將發(fā)送隊列頭的數(shù)據(jù)拷貝到緩沖區(qū)中,釋放一個阻塞的發(fā)送方;

5.2 緩沖區(qū)

當 Channel 的緩沖區(qū)中已經(jīng)包含數(shù)據(jù)時,從 Channel 中接收數(shù)據(jù)會直接從緩沖區(qū)中 的索引位置中取出數(shù)據(jù)進行處理:

5.3 阻塞接收

當 Channel 的發(fā)送隊列中不存在等待的 Goroutine 并且緩沖區(qū)中也不存在任何數(shù)據(jù)時,從管道中接收數(shù)據(jù)的操作會變成阻塞的,然而不是所有的接收操作都是阻塞的,與 select 語句結(jié)合使用時就可能會使用到非阻塞的接收操作:

六. 關(guān)閉channel

使用 close(ch) 來關(guān)閉 channel 最后會調(diào)用 runtime 中的 closechan 方法.

關(guān)閉一個 nil 的 channel 和已關(guān)閉了的 channel 都會導致 panic關(guān)閉 channel 后會釋放所有因為 channel 而阻塞的 Goroutine

七. 使用場景

channel一般用于協(xié)程之間的通信,channel也可以用于并發(fā)控制。比如主協(xié)程啟動N個子協(xié)程,主協(xié)程等待所有子協(xié)程退出后再繼續(xù)后續(xù)流程,這種場景下channel也可輕易實現(xiàn)。

7.1 使用channel控制子協(xié)程

package main
 
import (
    "time"
    "fmt"
)
 
func Process(ch chan int) {
    //Do some work...
    time.Sleep(time.Second)
 
    ch <- 1 //管道中寫入一個元素表示當前協(xié)程已結(jié)束
}
 
func main() {
    channels := make([]chan int, 10) //創(chuàng)建一個10個元素的切片,元素類型為channel
 
    for i:= 0; i < 10; i++ {
        channels[i] = make(chan int) //切片中放入一個channel
        go Process(channels[i])      //啟動協(xié)程,傳一個管道用于通信
    }
 
    for i, ch := range channels {  //遍歷切片,等待子協(xié)程結(jié)束
        <-ch
        fmt.Println("Routine ", i, " quit!")
    }
}

輸出:

Routine? 0? quit!

Routine? 1? quit!

Routine? 2? quit!

Routine? 3? quit!

Routine? 4? quit!

Routine? 5? quit!

Routine? 6? quit!

Routine? 7? quit!

Routine? 8? quit!

Routine? 9? quit!

上面程序通過創(chuàng)建N個channel來管理N個協(xié)程,每個協(xié)程都有一個channel用于跟父協(xié)程通信,父協(xié)程創(chuàng)建完所有協(xié)程后等待所有協(xié)程結(jié)束。

這個例子中,父協(xié)程僅僅是等待子協(xié)程結(jié)束,其實父協(xié)程也可以向管道中寫入數(shù)據(jù)通知子協(xié)程結(jié)束,這時子協(xié)程需要定期地探測管道中是否有消息出現(xiàn)。

7.2 通過關(guān)閉 channel 實現(xiàn)一對多的通知

關(guān)閉 channel 時會釋放所有阻塞的 Goroutine,所以我們就可以利用這個特性來做一對多的通知,除了一對多之外我們還用了 done 做了多對一的通知,當然多對一這種情況還是建議直接使用 WaitGroup 即可

package main
 
import (
	"fmt"
	"time"
)
 
func run(stop <-chan struct{}, done chan<- struct{}) {
 
	// 每一秒打印一次
	for {
		select {
		case <-stop:
			fmt.Println("stop...")
			// 接收到停止后,向 done 管道中發(fā)送數(shù)據(jù),然后退出函數(shù)
			done <- struct{}{}
			return
		// 超時1秒將輸出hello
		case <-time.After(time.Second):
			fmt.Println("hello...")
		}
	}
}
 
func main() {
	// 一對多,使用無緩沖通道,當關(guān)閉chan后,其他程序中接收到關(guān)閉信號后會統(tǒng)一執(zhí)行操作
	stop := make(chan struct{})
 
	// 多對一,當關(guān)閉后,關(guān)閉一個chan, 寫入一個數(shù)據(jù)到管道中
	done := make(chan struct{}, 10)
 
	for i := 0; i < 10; i++ {
		go run(stop, done)
	}
 
	// 模擬超時時間
	time.Sleep(5 * time.Second)
	close(stop)
 
	for i := 0; i < 10; i++ {
		<-done
	}
}

輸出:

hello...

hello...

hello...

...

hello..

stop...

stop...

stop...

stop...

stop...

stop...

stop...

stop...

stop...

stop...

7.3 使用 channel 做異步編程

利用無緩沖channel,接收早于發(fā)送的特點,只有當數(shù)據(jù)寫入后,接收才能完成實現(xiàn)數(shù)據(jù)一致性

package main
 
import (
	"fmt"
)
 
// 這里只能讀
func read(c <-chan int) {
	fmt.Println("read:", <-c)
}
 
// 這里只能寫
func write(c chan<- int) {
	c <- 0
}
 
func main() {
	c := make(chan int)
	go write(c)
	read(c)
}

7.4 超時控制

超時控制還是建議使用 context

func run(stop <-chan struct{}, done chan<- struct{}) {
	// 每一秒打印一次 hello
	for {
		select {
		case <-stop:
			fmt.Println("stop...")
			done <- struct{}{}
			return
		case <-time.After(time.Second):
			fmt.Println("hello")
		}
	}
}

7.5 協(xié)程池

根據(jù)控制Channel的緩存大小來控制并發(fā)執(zhí)行的Goroutine的最大數(shù)目

var limit = make(chan int, 3)
 
func main() {
    for _, w := range work {
        go func() {
            limit <- 1
            w()
            <-limit
        }()
    }
    select{}
}

最后一句select{}是一個空的管道選擇語句,該語句會導致main線程阻塞,從而避免程序過早退出。還有for{}、<-make(chan int)等諸多方法可以達到類似的效果。因為main線程被阻塞了,如果需要程序正常退出的話可以通過調(diào)用os.Exit(0)實現(xiàn)。

八. 參考

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/

https://www.topgoer.cn/docs/gozhuanjia/chapter055.1-channel

https://lailin.xyz/post/go-training-week3-channel.html

https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html

到此這篇關(guān)于Go并發(fā)編程中使用channel的方法的文章就介紹到這了,更多相關(guān)Go?channel使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java輸入輸出流實例詳解

    Java輸入輸出流實例詳解

    這篇文章主要介紹了Java輸入輸出流,結(jié)合實例形式詳細分析了Java常見的輸入輸出常用操作技巧與相關(guān)注意事項,需要的朋友可以參考下
    2018-09-09
  • logback如何自定義日志存儲

    logback如何自定義日志存儲

    這篇文章主要介紹了logback如何自定義日志存儲的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-08-08
  • Arrays.sort(arr)是什么排序及代碼邏輯

    Arrays.sort(arr)是什么排序及代碼邏輯

    在學習過程中觀察到Arrays.sort(arr)算法可以直接進行排序,但不清楚底層的代碼邏輯是什么樣子,今天通過本文給大家介紹下Arrays.sort(arr)是什么排序,感興趣的朋友一起看看吧
    2022-02-02
  • 23種設計模式(19)java責任鏈模式

    23種設計模式(19)java責任鏈模式

    這篇文章主要為大家詳細介紹了23種設計模式之java責任鏈模式,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • 聊聊@value注解和@ConfigurationProperties注解的使用

    聊聊@value注解和@ConfigurationProperties注解的使用

    這篇文章主要介紹了@value注解和@ConfigurationProperties注解的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • 解析SpringBoot?搭建基于?MinIO?的高性能存儲服務的問題

    解析SpringBoot?搭建基于?MinIO?的高性能存儲服務的問題

    Minio是Apache?License?v2.0下發(fā)布的對象存儲服務器,使用MinIO構(gòu)建用于機器學習,分析和應用程序數(shù)據(jù)工作負載的高性能基礎架構(gòu)。這篇文章主要介紹了SpringBoot?搭建基于?MinIO?的高性能存儲服務,需要的朋友可以參考下
    2022-03-03
  • Java靜態(tài)static與實例instance方法示例

    Java靜態(tài)static與實例instance方法示例

    這篇文章主要為大家介紹了Java靜態(tài)static與實例instance方法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-08-08
  • java對圖片進行壓縮和resize縮放的方法

    java對圖片進行壓縮和resize縮放的方法

    本篇文章主要介紹了java對圖片進行壓縮和resize調(diào)整的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-07-07
  • 關(guān)于SpringBoot單元測試(cobertura生成覆蓋率報告)

    關(guān)于SpringBoot單元測試(cobertura生成覆蓋率報告)

    這篇文章主要介紹了關(guān)于SpringBoot單元測試(cobertura生成覆蓋率報告),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Java排序算法三之歸并排序的遞歸與非遞歸的實現(xiàn)示例解析

    Java排序算法三之歸并排序的遞歸與非遞歸的實現(xiàn)示例解析

    這篇文章主要介紹了Java排序算法三之歸并排序的遞歸與非遞歸的實現(xiàn)示例解析,文章通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2020-08-08

最新評論