go並發之goroutine和channel,並發控制入門篇

並發的概念及其重要性

這段是簡單科普,大佬可以跳過

並發:並發程序指同時進行多個任務的程序。在操作系統中,是指一個時間段中有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行。

———-本段引用內容源自《GO語言高級編程》

在早期,CPU都是以單核的形式順序執行機器指令。Go語言的祖先C語言正是這種順序編程語言的代表。順序編程語言中的順序是指:所有的指令都是以串行的方式執行,在相同的時刻有且僅有一個CPU在順序執行程序的指令。

隨着處理器技術的發展,單核時代以提升處理器頻率來提高運行效率的方式遇到了瓶頸,目前各種主流的CPU頻率基本被鎖定在了3GHZ附近。單核CPU的發展的停滯,給多核CPU的發展帶來了機遇。相應地,編程語言也開始逐步向並行化的方向發展。Go語言正是在多核和網絡化的時代背景下誕生的原生支持並發的編程語言。

在聊並發之前,聊聊共享變量、線程、協程

  1. 如何在不同線程/協程 共享 變量/內存?

這裡留給各位看官去自行查資料,即使我列出來也不如自己動手去查記憶深刻!

不想查也可以等我下一篇文章,更加詳細解讀線程、進程。

  1. 線程和協程概念?

線程:線程是操作系統能夠進行運算調度的最小單位。一個進程可以包含多個線程,是進程中的實際運作單位。

協程:又稱微線程。協程是一種用戶態的輕量級線程。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。

  1. 為什麼會誕生協程?

雖然多線程在前互聯網世代已經足夠使用,但是線程的局限性也比較明顯

  1. 線程數量有限,一般不會很多
  2. 線程佔據的資源通常比我們需要的多得多,造成浪費

每個系統級線程開闢都會佔用空間,這個空間可能是MB級別,但是我們如果使用的線程只需要傳遞KB級別數據,那麼線程看起來就會比較浪費,但是又不可避免。而且線程之間的切換也會佔用一些額外開銷。

為了解決上面的矛盾問題,協程誕生了:更小的資源開支,動態調配資源,比線程更輕量。

協程的一些優點:

  1. 因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
  2. 不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。

在golang中,goroutine的創建消耗非常小,大約是KB級別。因此可以創建更多的協程,尤其是數量越多相對線程優勢更加明顯,而且goroutine可以動態伸縮,棧溢出風險也比線程更低。

golang的並發,goroutine的使用

var name = "yiyiyinhe"

func changeName() {
    name = "change"
}

func sayHi() {
    fmt.println("hi, ", name)
    go changeName() // 協程
}

簡單的協程就創建了,那麼打印出來的結果可能是hi, yiyiyinhe也可能是hi, change

如果想對某一代碼塊執行協程而不是某個方法,則使用下面方式

var name = "yiyiyinhe"

func sayHi() {
    fmt.println("hi, ", name)
    go func() { // 匿名函數執行協程
        name = "change"
    }
}

channel

golang對共享變量的口號

Do not communicate by sharing memory; instead, share memory by communicating.

不要通過共享內存來通信,而應通過通信來共享內存。

那麼在協程中也需要進行通信,而golang使用的goroutine之間通信和同步的主要方法是channel。

什麼是channel呢?

A channel is a communic ation mechanism that lets one goroutine send values to another goroutine. Each channel is a conduit for values of a particular type, called the channel』s element type.

channel是一種通信機制,它讓一個goroutine向另一個goroutine發送值。每個通道都是特定類型(通道元素類型)值的管道。

簡單來理解就是,channel是用於在goroutine中進行通信的管道,而且管道是有特定類型的。

創建的channel分為有緩存和無緩存兩種,區別就是創建的時候是否分配大小

  • 無緩存channel

    • var ch1 = make(chan int),未分配大小
    • var ch2 = make(chan int, 0),分配大小為0也等同給於未分配大小
  • 有緩存channel

    • var :ch3 = make(int, 3),分配大小為3的有緩存channel

無緩存channel中,channel的發送操作總是在接收之前發生;簡單理解就是,無緩存channel是一個管道必須從頭flag<-true發送到尾部<-flag,而且尾部發生的時間一定是在頭部發送之後。

  • 為什麼chennel可以這樣呢?

    因為channel有阻塞作用,必須接收了才能繼續下去。

channel

有緩存channel則不具備上述的特性,因為對於帶緩衝的Channel,對於Channel的第 K 個接收完成操作發生在第 K+C 個發送操作完成之前,其中 C 是Channel的緩存大小。 如果將 C 設置為0自然就對應無緩存的Channel,也即使第K個接收完成在第K個發送完成之前。因為無緩存的Channel只能同步發1個,也就簡化為前面無緩存Channel的規則:對於從無緩衝Channel進行的接收,發生在對該Channel進行的發送完成之前。

還是上面的例子,使用channel來演示一下

var name = "yiyi"
var flag = make(chan bool) // 創建了bool類型的channel

func changeName() {
    name = "change"
    flag <- true  // 發送
}

func sayHi() {
    go changeName() // 協程
    <-flag // 接收
    fmt.println("hi, ", name)
}

那麼這個時候打印出來的就是一個固定的順序,由於<-flag接收總是在發送之後執行,因此當flag <- true執行完之前name = "change"已經執行,打印結果一定是:hi, change

上面代碼等同於下圖所示

看完你可以收穫什麼?

  1. 簡單了解並發,了解多線程簡單的發展來歷
  2. 簡單了解線程,協程
  3. 為什麼協程會誕生?
  4. goroutine的兩種使用方式
  5. channel是什麼?channel的兩種分類;
  6. channel在goroutine中有什麼作用?

寫在最後

由於我剛開始寫技術文章,很多東西不知道怎麼寫才能讓大家都看懂,就像寫線程協程的時候不知道要不要解釋共享變量或者共享內存是什麼,也不知道大家能不能知道多線程模式下線程之間通信有哪些方式,感覺都想寫但是又覺得大家看文章標題應該是了解一些東西的,如果都寫篇幅太長,不寫又看不懂;就覺得比較矛盾吧,也希望大家能夠給我提一些意見建議!

作者還在慢慢努力,盡量把文章寫的通俗易懂,排版準確,抓住重點,把最好的內容展現給大家。