Golang並發編程基礎

硬體

記憶體

作為並發編程一個基礎硬體知識儲備,首先要說的就是記憶體了,總的來說在絕大多數情況下把記憶體的並發增刪改查模型搞清楚了其他的基本上也是異曲同工之妙。

記憶體晶片——即我們所知道的記憶體顆粒,是一堆MOS管的集合,在半導體稱呼裡面,很多MOS管組成一個半導體(組module),很多個module組成一個管芯(die),這個die即是記憶體顆粒,當然,更上一級即很多die組成的東西叫做晶圓(wafer)。

簡單來說,每8個MOS管組成的電路可以表示一個位元組,比如ASCII的『A』,我們使用65表示,即0100 0001,那麼8個MOS分別使用低-高-低-低-低-低-低-高電位即可表示字元A。

在對記憶體的寫入和讀取時,通常也是按照8個字開始作為一組進行操作,我們現在常用的CPU是64位,可以一次性處理64/8=8個位元組的數據。

匯流排

首先明確一個概念:匯流排是線但是也不是線,以下是來自百科的解釋:

匯流排(Bus)是電腦各種功能部件之間傳送資訊的公共通訊幹線,它是由導線組成的傳輸線束, 按照電腦所傳輸的資訊種類,電腦的匯流排可以劃分為數據匯流排、地址匯流排和控制匯流排,分別用來傳輸數據、數據地址和控制訊號。匯流排是一種內部結構,它是cpu、記憶體、輸入、輸出設備傳遞資訊的公用通道。

一個CPU要操作記憶體的數據,是通過匯流排來進行操作的,通常來說記憶體的讀寫操作不是一個CPU指令周期能完成的,如果多個程式在同時操作一個記憶體地址,則有各種意外的讀寫操作。

CPU

在單核CPU時期,硬體一次只能處理一個事情,在多任務的情況下不同的任務按需搶佔CPU來執行它的程式碼,這裡面就涉及到CPU調度工作,通常情況下,作業系統已經幫我們做了很多事,如果一個程式語言開啟的並發操作是交給了作業系統的,那麼調度這塊不需要太關心,如果像Go這樣有自己的協程調度器,還是需要專門了解下特有的調度方式的。

多核時期,基本原理也差不多,在對於硬體的理解上也可以完全參考單核。

CPU通過地址匯流排去尋找記憶體地址,比如0x00004567這種,64位CPU最大能操作的地址長度為264,32位作業系統則是232長度,所以為什麼32位CPU最大隻支援4GB記憶體呢?

幾個程式碼示例

示例一

package main

import (
    "fmt"
)

var A int
func main() {
A = 0
for i:=0;i<100;i++{
    A++
}
fmt.Println(A)
}

示例二

package main

import (
    "fmt"
    "time"
)

var A int
func main() {
A = 0
for j:=0;j<100;j++{
    go add()
}
time.Sleep(1*time.Second)
fmt.Println(A)
}

func add(){
A++
return
}

示例一個示例二都將輸出什麼呢,直接告訴大家結果吧:絕大多數情況下都是100

那麼go的協程難道這麼聽話,我們就完全很happy地編碼了嗎?先把示例二的100改成10000再試試吧_

我們再看看示例三和示例四:

示例三

package main

import (
    "fmt"
)

func main() {
for i:=0;i<10000;i++{
fmt.Println(i)
}
}

示例四

package main

import (
    "fmt"
    "time"
)

func main() {
for j:=0;j<10000;j++{
    go add(j)
}
time.Sleep(1*time.Second)
}

func add(j int){
fmt.Println(j)
return
}

示例三其實沒太多好說的,單協程模型,輸出也不會有什麼意外,而示例四大家猜猜是按照1,2,3…9999這樣的順序呢還是其他輸出順序呢?

綜上結果,我們會發現多協程模型裡面的東西沒有順序性,對變數的操作也沒有原子性。

示例五給出了Golang中最簡單的加鎖處理方式:

示例五

package main

import (
    "fmt"
    "time"
    "sync"
)

var A int
var LOCK *sync.Mutex

func main() {
A = 0
LOCK = new(sync.Mutex)
for j:=0;j<10000;j++{
    go add()
}
time.Sleep(1*time.Second)
fmt.Println(A)
}

func add(){
LOCK.Lock()
A++
LOCK.Unlock()
return
}

而關於多協程順序性方面的實現方式,也可以比著葫蘆畫瓢寫出來,這裡就不再贅述了。

搬磚例子

假設在左邊有三堆散亂的磚,我們需要將其從左邊搬運到右邊並堆放整齊,這樣的一個工作我們從並發模型來看有哪些比較可執行的實現方式呢:

  1. 每堆磚頭分配固定的人數,堆磚時為保證堆疊整齊度,採用排隊的方式一個一個按先後順序堆疊
  2. 拿一個人專職在左邊遞磚,若干人從左邊的遞磚人處拿磚,搬磚後在右邊排隊堆疊
  3. 左邊專人遞磚,右邊專人堆磚,若干搬磚人只負責搬磚

這也是並發編程模型中比較常用的編程思路,在以後遇到類似問題的時候可以想想這個例子。

一個實際案例

我們以一個實際的案例作為結束,這個案例是導出某雲平台所屬設備資訊的程式碼,裡面包含有多協程拉取數據的實例,整體的流程如下:

  1. 參數初始化
  2. 定義一個接收協程結束的資訊通道
  3. 開啟N個協程
  4. 協程調用API獲取資訊,按分頁參數每個協程獲取(總數/N)資訊,每次page=X+N
  5. 每次獲取的資訊放入excel緩衝區
  6. 當最後的分頁獲取不到資訊時向通道寫入東西表示該協程任務完成
  7. 主進程循環獲取每個協程結束的資訊,直到所有協程任務完成
  8. 將excel緩衝區數據寫入excel文件
  9. 結束

連接如下:
//github.com/cm-heclouds/onenet_device_export/releases/tag/2018-latest
當然,這個案例在並發上其實還存在較大的提升空間,聰明的大家看看結合搬磚的例子來怎麼提升呢。

Tags: