1. 程式人生 > >Golang 併發程式設計總結

Golang 併發程式設計總結

   Golang :不要通過共享記憶體來通訊,而應該通過通訊來共享記憶體。這句風靡在Go社群的話,說的就是 goroutine中的 channel …….   他在go併發程式設計中充當著 型別安全的管道作用。

1.通過golang中的 goroutine 與sync.Mutex進行 併發同步

import( 
   "fmt"
   "sync"
   "runtime"
)
var count int =0;
func counter(lock * sync.Mutex){
     lock.Lock()
     count++
     fmt.Println(count)
     lock.Unlock()
}
func
main()
{
  lock:=&sync.Mutex{}
  for i:=0;i<10;i++{
     //傳遞指標是為了防止 函式內的鎖和 呼叫鎖不一致
     go counter(lock)  
    }
  for{
     lock.Lock()
     c:=count
     lock.Unlock()
     ///把時間片給別的goroutine  未來某個時刻執行該routine
     runtime.Gosched()
     if c>=10{
       fmt.Println("goroutine end")
       break

       }
  }    
}

2、goroutine之間通過 channel進行通訊,channel是和型別相關的 可以理解為  是一種型別安全的管道。

    簡單的channel 使用

package main  
import "fmt"
func Count(ch chan int) {
   ch <- 1  
   fmt.Println("Counting")
}
func main() {
   chs := make([]chan int, 10)
for i := 0; i < 10; i++ {
       chs[i] = make(chan int)
 go Count(chs[i])
 fmt.Println("Count"
,i)
   }
for i, ch := range chs {
 <-ch
 fmt.Println("Counting",i)
   }  
}

3、Go語言中的select是語言級內建  非堵塞

select {
case <-chan1: // 如果chan1成功讀到資料,則進行該case處理語句  
case chan2 <- 1: // 如果成功向chan2寫入資料,則進行該case處理語句  
default: // 如果上面都沒有成功,則進入default處理流程  
}

    可以看出,select不像switch,後面並不帶判斷條件,而是直接去檢視case語句。每個case語句都必須是一個面向channel的操作。比如上面的例子中,第一個case試圖從chan1讀取一個數據並直接忽略讀到的資料,而第二個case則是試圖向chan2中寫入一個整型數1,如果這兩者都沒有成功,則到達default語句。

4、channel 的帶緩衝讀取寫入

     之前我們示範建立的都是不帶緩衝的channel,這種做法對於傳遞單個數據的場景可以接受,但對於需要持續傳輸大量資料的場景就有些不合適了。接下來我們介紹如何給channel帶上緩衝,從而達到訊息佇列的效果。

    要建立一個帶緩衝的channel,其實也非常容易:

c := make(chan int, 1024)

    在呼叫make()時將緩衝區大小作為第二個引數傳入即可,比如上面這個例子就建立了一個大小為1024的int型別channel,即使沒有讀取方,寫入方也可以一直往channel裡寫入,在緩衝區被填完之前都不會阻塞。

   從帶緩衝的channel中讀取資料可以使用與常規非緩衝channel完全一致的方法,但我們也可以使用range關鍵來實現更為簡便的迴圈讀取:

for i := range c {
   fmt.Println("Received:", i)
}

5、用goroutine模擬生產消費者

package main
import "fmt"
import "time"
func Producer (queue chan<- int){
       for i:= 0; i < 10; i++ {
               queue <- i  
               }
}
func Consumer( queue <-chan int){
       for i :=0; i < 10; i++{
               v := <- queue
               fmt.Println("receive:", v)
       }
}
func main(){
       queue := make(chan int, 1)
       go Producer(queue)
       go Consumer(queue)
       time.Sleep(1e9) //讓Producer與Consumer完成
}

6、 通過make 建立通道

 make(c1 chan int)   建立的是 同步channel ...讀寫完全對應
make(c1 chan int ,10) 闖進帶緩衝的通道 上來可以寫10

7、隨機向通道中寫入0或者1

package main
import "fmt"
import "time"
func main(){
      ch := make(chan int, 1)
for {
  ///不停向channel中寫入 0 或者1
 select {
  case ch <- 0:
  case ch <- 1:
 }
   //從通道中取出資料
   i := <-ch
   fmt.Println("Value received:",i)
   time.Sleep(1e8)
   }
}

8、帶緩衝的channel

     之前建立的都是不帶緩衝的channel,這種做法對於傳遞單個數據的場景可以接受,但對於需要持續傳輸大量資料的場景就有些不合適了。接下來我們介紹如何給channel帶上緩衝,從而達到訊息佇列的效果。

    要建立一個帶緩衝的channel,其實也非常容易:

c := make(chan int, 1024)

    在呼叫make()時將緩衝區大小作為第二個引數傳入即可,比如上面這個例子就建立了一個大小為1024的int型別channel,即使沒有讀取方,寫入方也可以一直往channel裡寫入,在緩衝區被填完之前都不會阻塞。從帶緩衝的channel中讀取資料可以使用與常規非緩衝channel完全一致的方法,但我們也可以使用range關鍵來實現更為簡便的迴圈讀取:

for i := range c {
   fmt.Println("Received:", i)
}

////////////////////////////////////////下面是測試程式碼////////////////////////////////////

package main
import "fmt"
import "time"
func A(c chan int){
for i:=0;i<10;i++{
       c<- i
   }
}
func B(c chan int){
for val:=range c {
     fmt.Println("Value:",val)  
   }
}
func main(){
   chs:=make(chan int,10)
   //只要有通道操作一定要放到goroutine中否則 會堵塞當前的主執行緒 並且導致程式退出
   //對於同步通道 或者帶緩衝的通道 一定要封裝成函式 使用 goroutine 包裝
   go A(chs)
   go B(chs)
   time.Sleep(1e9)
}

9、關於建立多個goroutine具體到go語言會建立多少個執行緒

import "os"
func main() {
   for i:=0; i<20; i++ {
       go func() {
           for {
               b:=make([]byte, 10)
               os.Stdin.Read(b) // will block
           }
       }()
   }
   select{}
}

    會產生21個執行緒:

runtime scheduler(src/pkg/runtime/proc.c)會維護一個執行緒池,當某個goroutine被block後,scheduler會建立一個新執行緒給其他ready的goroutine
GOMAXPROCS控制的是未被阻塞的所有goroutine被multiplex到多少個執行緒上執行

10、在channel中也是可以傳遞channel的,Go語言的channel和map  slice等一樣都是原生型別

    需要注意的是,在Go語言中channel本身也是一個原生型別,與map之類的型別地位一樣,因此channel本身在定義後也可以通過channel來傳遞。我們可以使用這個特性來實現*nix上非常常見的管道(pipe)特性。管道也是使用非常廣泛的一種設計模式,比如在處理資料時,我們可以採用管道設計,這樣可以比較容易以外掛的方式增加資料的處理流程。

    下面我們利用channel可被傳遞的特性來實現我們的管道。 為了簡化表達, 我們假設在管道中傳遞的資料只是一個整型數,在實際的應用場景中這通常會是一個數據塊。

    首先限定基本的資料結構:

type PipeData struct {
   value int
   handler func(int) int
   next chan int
}

    然後我們寫一個常規的處理函式。我們只要定義一系列PipeData的資料結構並一起傳遞給這個函式,就可以達到流式處理資料的目的:

func handle(queue chan *PipeData) {
for data := range queue {
       data.next <- data.handler(data.value)
   }
}

11、我們預設建立的是雙向通道,單向通道沒有意義,但是我們卻可以通過強制轉換 將雙向通道 轉換成為單向通道 。

var ch1 chan int  // ch1是一個正常的channel,不是單向的  
var ch2 chan<- float64// ch2是單向channel,只用於寫float64資料
var ch3 <-chan int // ch3是單向channel,只用於讀取int資料
channel是一個原生型別,因此不僅 支援被傳遞,還支援型別轉換。只有在介紹了單向channel的概念後,讀者才會明白型別轉換對於
channel的意義:就是在單向channel和雙向channel之間進行轉換。
示例如下:
ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一個單向的讀取channel
ch6 := chan<- int(ch4) // ch6 是一個單向的寫入channel

    基於ch4,我們通過型別轉換初始化了兩個單向channel:單向讀的ch5和單向寫的ch6。從設計的角度考慮,所有的程式碼應該都遵循“最小許可權原則” ,從而避免沒必要地使用氾濫問題, 進而導致程式失控。 寫過C++程式的讀者肯定就會聯想起const 指標的用法。非const指標具備const指標的所有功能,將一個指標設定為const就是明確告訴函式實現者不要試圖對該指標進行修改。單向channel也是起到這樣的一種契約作用。

    下面我們來看一下單向channel的用法:

func Parse(ch <-chan int) {
for value := range ch {
       fmt.Println("Parsing value", value)  
   }
}

    除非這個函式的實現者無恥地使用了型別轉換,否則這個函式就不會因為各種原因而對ch 進行寫,避免在ch中出現非期望的資料,從而很好地實踐最小許可權原則。

12、只讀只寫 單向 channel 程式碼例子    遵循許可權最小化的原則

package main
import "fmt"
import "time"
//接受一個引數 是隻允許讀取通道  除非直接強制轉換 要麼你只能從channel中讀取資料
func sCh(ch <-chan int){
  for val:= range ch {
    fmt.Println(val)
  }
}
func main(){
   //建立一個帶100緩衝的通道 可以直接寫入 而不會導致 主執行緒堵塞
   dch:=make(chan int,100)
   for i:=0;i<100;i++{
     dch<- i  
   }
   //傳遞進去 只讀通道
   go sCh(dch)
   time.Sleep(1e9)
}

13、channel的關閉,以及判斷channel的關閉

    關閉channel非常簡單,直接使用Go語言內建的close()函式即可:

close(ch)

   在介紹瞭如何關閉channel之後,我們就多了一個問題:如何判斷一個channel是否已經被關閉?我們可以在讀取的時候使用多重返回值的方式:

x, ok := <-ch

   這個用法與map中的按鍵獲取value的過程比較類似,只需要看第二個bool返回值即可,如果返回值是false則表示ch已經被關閉。

14、Go的多核並行化程式設計    

    高效能併發程式設計 必須設定GOMAXPROCS 為最大核數目 這個值由runtime.NumCPU()獲取在執行一些昂貴的計算任務時, 我們希望能夠儘量利用現代伺服器普遍具備的多核特性來盡量將任務並行化,從而達到降低總計算時間的目的。此時我們需要了解CPU核心的數量,並針對性地分解計算任務到多個goroutine中去並行執行。

   下面我們來模擬一個完全可以並行的計算任務:計算N個整型數的總和。我們可以將所有整型數分成M份,M即CPU的個數。讓每個CPU開始計算分給它的那份計算任務,最後將每個CPU的計算結果再做一次累加,這樣就可以得到所有N個整型數的總和:

type Vector []float64

// 分配給每個CPU的計算任務

func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1      
// 發訊號告訴任務管理者我已經計算完成了
}
const NCPU = 16    
// 假設總共有16核  
func (v Vector) DoAll(u Vector) {  
   c := make(chan int, NCPU)  // 用於接收每個CPU的任務完成訊號  
for i := 0; i < NCPU; i++ {  
go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
   }
// 等待所有CPU的任務完成
for i := 0; i < NCPU; i++ {  
<-c    // 獲取到一個數據,表示一個CPU計算完成了
   }
// 到這裡表示所有計算已經結束
}

   這兩個函式看起來設計非常合理。DoAll()會根據CPU核心的數目對任務進行分割,然後開闢多個goroutine來並行執行這些計算任務。是否可以將總的計算時間降到接近原來的1/N呢?答案是不一定。如果掐秒錶(正常點的話,應該用7.8節中介紹的Benchmark方法) ,會發現總的執行時間沒有明顯縮短。再去觀察CPU執行狀態, 你會發現儘管我們有16個CPU核心, 但在計算過程中其實只有一個CPU核心處於繁忙狀態,

   這是會讓很多Go語言初學者迷惑的問題。

   官方的答案是,這是當前版本的Go編譯器還不能很智慧地去發現和利用多核的優勢。雖然我們確實建立了多個goroutine,並且從執行狀態看這些goroutine也都在並行執行,但實際上所有這些goroutine都執行在同一個CPU核心上, 在一個goroutine得到時間片執行的時候, 其他goroutine都會處於等待狀態。從這一點可以看出,雖然goroutine簡化了我們寫並行程式碼的過程,但實際上整體執行效率並不真正高於單執行緒程式。

   在Go語言升級到預設支援多CPU的某個版本之前,我們可以先通過設定環境變數GOMAXPROCS的值來控制使用多少個CPU核心。具體操作方法是通過直接設定環境變數GOMAXPROCS的值,或者在程式碼中啟動goroutine之前先呼叫以下這個語句以設定使用16個CPU核心:

runtime.GOMAXPROCS(16)

   到底應該設定多少個CPU核心呢,其實runtime包中還提供了另外一個函式NumCPU()來獲取核心數。可以看到,Go語言其實已經感知到所有的環境資訊,下一版本中完全可以利用這些資訊將goroutine排程到所有CPU核心上,從而最大化地利用伺服器的多核計算能力。拋棄GOMAXPROCS只是個時間問題。

15、主動出讓時間片給其他 goroutine 在未來的某一時刻再來執行當前goroutine

    我們可以在每個goroutine中控制何時主動出讓時間片給其他goroutine,這可以使用runtime包中的Gosched()函式實現。實際上,如果要比較精細地控制goroutine的行為,就必須比較深入地瞭解Go語言開發包中runtime包所提供的具體功能。

16、Go中的同步

    倡導用通訊來共享資料,而不是通過共享資料來進行通訊,但考慮到即使成功地用channel來作為通訊手段,還是避免不了多個goroutine之間共享資料的問題,Go語言的設計者雖然對channel有極高的期望,但也提供了妥善的資源鎖方案。

17、Go中的同步鎖

   倡導用通訊來共享資料,而不是通過共享資料來進行通訊,但考慮到即使成功地用channel來作為通訊手段,還是避免不了多個goroutine之間共享資料的問題,Go語言的設計者雖然對channel有極高的期望,但也提供了妥善的資源鎖方案。

   對於這兩種鎖型別, 任何一個Lock()或RLock()均需要保證對應有Unlock()或RUnlock()呼叫與之對應,否則可能導致等待該鎖的所有goroutine處於飢餓狀態,甚至可能導致死鎖。鎖的典型使用模式如下:

var l sync.Mutex  
func foo() {
l.Lock()  
//延遲呼叫 在函式退出 並且區域性資源被釋放的時候 呼叫
defer l.Unlock()  
//...
}  

    這裡我們再一次見證了Go語言defer關鍵字帶來的優雅

18、全域性唯一操作 sync.Once.Do()     sync.atomic原子操作子包

    對於從全域性的角度只需要執行一次的程式碼,比如全域性初始化操作,Go語言提供了一個Once型別來保證全域性的唯一性操作,具體程式碼如下:

var a string
var once sync.Once  
func setup() {
a = "hello, world"
}  
func doprint() {
once.Do(setup)
print(a)  
}  
func twoprint() {
go doprint()
go doprint()  
}

    如果這段程式碼沒有引入Once, setup()將會被每一個goroutine先呼叫一次, 這至少對於這個例子是多餘的。在現實中,我們也經常會遇到這樣的情況。Go語言標準庫為我們引入了Once類型以解決這個問題。once的Do()方法可以保證在全域性範圍內只調用指定的函式一次(這裡指setup()函式) ,而且所有其他goroutine在呼叫到此語句時,將會先被阻塞,直至全域性唯一的once.Do()呼叫結束後才繼續。

     這個機制比較輕巧地解決了使用其他語言時開發者不得不自行設計和實現這種Once效果的難題,也是Go語言為併發性程式設計做了儘量多考慮的一種體現。如果沒有once.Do(),我們很可能只能新增一個全域性的bool變數,在函式setup()的最後一行將該bool變數設定為true。在對setup()的所有呼叫之前,需要先判斷該bool變數是否已經被設定為true,如果該值仍然是false,則呼叫一次setup(),否則應跳過該語句。實現程式碼

var done bool = false
func setup() {
a = "hello, world"
done = true
}    
func doprint() {
if !done {
       setup()
   }  
print(a)  
}  

    這段程式碼初看起來比較合理, 但是細看還是會有問題, 因為setup()並不是一個原子性操作,這種寫法可能導致setup()函式被多次呼叫,從而無法達到全域性只執行一次的目標。這個問題的

相關推薦

Golang 併發程式設計總結

   Golang :不要通過共享記憶體來通訊,而應該通過通訊來共享記憶體。這句風靡在Go社群的話,說的就是 goroutine中的 channel …….   他在go併發程式設計中充當著 型別安全的

學習筆記 --- Java 併發程式設計總結二 ThreadLocal

ThreadLocal是什麼 ThreadLocal是一個本地執行緒副本變數工具類。主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高併發場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不通的變數值完成操作的場景。 下圖為Threa

學習筆記 --- Java 併發程式設計總結一 countDownLatch,CyclicBarrier,Semaphore

在多執行緒程式設計中有三個同步工具需要我們掌握,分別是Semaphore(訊號量),countDownLatch(倒計數門閘鎖),CyclicBarrier(可重用柵欄) CountDownLatch和CyclicBarrier都能夠實現執行緒之間的等待,只不過它們側重點不同:  

Go併發程式設計總結

最近一直在看Go語言的併發程式設計,主要的書籍是 <Go語言學習筆記>。看完之後,看到網上這篇部落格總結的不錯,所講的內容學習筆記上都有。Golang :不要通過共享記憶體來通訊,而應該通過通訊來共享記憶體。這句風靡在Go社群的話,說的就是 goroutine中的

Go語言併發程式設計總結

Golang :不要通過共享記憶體來通訊,而應該通過通訊來共享記憶體。這句風靡在Go社群的話,說的就是 goroutine中的 channel ....... 他在go併發程式設計中充當著 型別安全的管道作用。 1、通過golang中的 goroutine 與sync

Golang從入門到精通(十八):Golang併發程式設計之Goroutine

程序,執行緒,並行和併發 一個應用程式是執行在機器上的一個程序;程序是一個執行在自己記憶體地址空間裡的獨立執行體。一個程序由一個或多個作業系統執行緒組成,這些執行緒其實是共享同一個記憶體地址空間的一起工作的執行體。幾乎所有’正式’的程式都是多執行緒的,以便讓使

Java併發程式設計總結4——ConcurrentHashMap在jdk1.8中的改進

一、簡單回顧ConcurrentHashMap在jdk1.7中的設計     先簡單看下ConcurrentHashMap類在jdk1.7中的設計,其基本結構如圖所示: 每一個segment都是一個HashEntry<K,V>[] table, table中的每一個元素本質上都是一個Has

Golang從入門到精通(十九):Golang併發程式設計之Channel

Go語言併發模型 Go 語言中使用了CSP模型來進行執行緒通訊,準確說,是輕量級執行緒goroutine之間的通訊。CSP模型和Actor模型類似,也是由獨立的,併發執行的實體所構成,實體之間也是通過傳送訊息進行通訊的。 Actor模型和CSP模型區別 A

java高併發程式設計總結三:JDK併發包之ReentrantLock重入鎖

為了更好的支援併發程式,jdk內部提供了大量實用的API和框架,重入鎖就是一種對同步的擴充套件 ReentrantLock起源 在1.5的時候,synchronized關鍵的效能不是很好,這也是concurrent併發包出現的一種潛在原因,而新出

golang併發程式設計的兩種限速方法

引子 golang提供了goroutine快速實現併發程式設計,在實際環境中,如果goroutine中的程式碼要消耗大量資源時(CPU、記憶體、頻寬等),我們就需要對程式限速,以防止goroutine將資源耗盡。 以下面虛擬碼為例,看看goroutine如何

golang併發程式設計

在早期,CPU都是以單核的形式順序執行機器指令。C語言、PHP正是這種順序程式語言的代表,即所有的指令都是以序列的方式執行,在相同的時刻有且僅有一個CPU在順序執行程式的指令。隨著處理器技術的發展,單核時代以提升處理器頻率來提高執行效率的方式遇到了瓶頸。單核CPU的發展的停滯,給多核CPU的發展帶來了機遇。相

Golang併發程式設計基礎

# 硬體 ## 記憶體 作為併發程式設計一個基礎硬體知識儲備,首先要說的就是記憶體了,總的來說在絕大多數情況下把記憶體的併發增刪改查模型搞清楚了其他的基本上也是異曲同工之妙。 記憶體晶片——即我們所知道的記憶體顆粒,是一堆MOS管的集合,在半導體稱呼裡面,很多MOS管組成一個半導體(組module),很多個

併發程式設計面試必備:JUC 中的 Atomic 原子類總結

個人覺得這一節掌握基本的使用即可! 本節思維導圖: 1 Atomic 原子類介紹 Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小單位,在化學反應中是不可分割的。在我們這裡 Atomic 是指一個操作是不可中斷的。即使是在多個執行緒一起執行的時

Java併發程式設計完整總結

Java併發程式設計系列:    【Java併發程式設計】實現多執行緒的兩種方法    【Java併發程式設計】執行緒的中斷    【Java併發程式設計】正確掛起、恢復、終止執行緒   &n

併發程式設計模型總結

一:並行工作者模型 並行工作模型主要是有多個工作者,每個工作者單獨完成一個事件。 如下圖 委派器將任務分配給Worker,Worker單獨完成任務,java 7 中 java.util.concurrent 包中好多工具都是基於此模型實現的。明視訊記憶體在的存在的問題是工作器之間需

Java併發(十八):阻塞佇列BlockingQueue BlockingQueue(阻塞佇列)詳解 二叉堆(一)之 圖文解析 和 C語言的實現 多執行緒程式設計:阻塞、併發佇列的使用總結 Java併發程式設計:阻塞佇列 java阻塞佇列 BlockingQueue(阻塞佇列)詳解

阻塞佇列(BlockingQueue)是一個支援兩個附加操作的佇列。 這兩個附加的操作是:在佇列為空時,獲取元素的執行緒會等待佇列變為非空。當佇列滿時,儲存元素的執行緒會等待佇列可用。 阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元素的執行緒,消費者是從佇列裡拿元素的執行緒。阻塞佇列就是生產者

《實戰Java高併發程式設計》學習總結(3)

第6章  java8與併發 1 顯式函式指函式與外界交換資料的唯一渠道就是引數和返回值,顯式函式不會去讀取或者修改函式的外部狀態。這樣的函式對於除錯和排錯是有益的。 2 函數語言程式設計式申明式的程式設計方式。而命令式則喜歡大量使用可變物件和指令。如下 // 指令式程式設計 p

《實戰Java高併發程式設計》學習總結(2)

第3章  JDK併發包 1 synchronized的功能擴充套件:重入鎖。使用java.util.concurrent.locks.ReentrantLock類來實現。 import java.util.concurrent.locks.ReentrantLock; publi

《實戰Java高併發程式設計》學習總結(1)

第1章 走入並行世界 1 併發(Concurrency)和並行(Parallelism)都可以表示兩個或多個任務一起執行。但併發偏重於多個任務交替執行,而多個任務之間有可能還是序列。並行是真正意義上的“同時執行”。 2 有關並行的兩個重要定律。Amdahl定律強調當序列比例一定時,加速比是有

Java併發程式設計和高併發學習總結(一)-大綱

系列 開篇語 想寫這樣一個東西很久了,在慕課網上學完某老師的課程(避免打廣告的嫌疑就不貼出來了,感興趣的同學可以去慕課網上去搜來看看,是個付費課程)之後就覺得應該有這樣的一個學習總結的東西來,後來因為懶又有其他事情耽誤了,然後又上了新專案(正好拿來練手了,當然