硬體
記憶體
作為併發程式設計一個基礎硬體知識儲備,首先要說的就是記憶體了,總的來說在絕大多數情況下把記憶體的併發增刪改查模型搞清楚了其他的基本上也是異曲同工之妙。
記憶體晶片——即我們所知道的記憶體顆粒,是一堆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
}
而關於多協程順序性方面的實現方式,也可以比著葫蘆畫瓢寫出來,這裡就不再贅述了。
搬磚例子
假設在左邊有三堆散亂的磚,我們需要將其從左邊搬運到右邊並堆放整齊,這樣的一個工作我們從併發模型來看有哪些比較可執行的實現方式呢:
- 每堆磚頭分配固定的人數,堆磚時為保證堆疊整齊度,採用排隊的方式一個一個按先後順序堆疊
- 拿一個人專職在左邊遞磚,若干人從左邊的遞磚人處拿磚,搬磚後在右邊排隊堆疊
- 左邊專人遞磚,右邊專人堆磚,若干搬磚人只負責搬磚
這也是併發程式設計模型中比較常用的程式設計思路,在以後遇到類似問題的時候可以想想這個例子。
一個實際案例
我們以一個實際的案例作為結束,這個案例是匯出某雲平臺所屬裝置資訊的程式碼,裡面包含有多協程拉取資料的例項,整體的流程如下:
- 引數初始化
- 定義一個接收協程結束的資訊通道
- 開啟N個協程
- 協程呼叫API獲取資訊,按分頁引數每個協程獲取(總數/N)資訊,每次page=X+N
- 每次獲取的資訊放入excel緩衝區
- 當最後的分頁獲取不到資訊時向通道寫入東西表示該協程任務完成
- 主程式迴圈獲取每個協程結束的資訊,直到所有協程任務完成
- 將excel緩衝區資料寫入excel檔案
- 結束
連線如下:
https://github.com/cm-heclouds/onenet_device_export/releases/tag/2018-latest
當然,這個案例在併發上其實還存在較大的提升空間,聰明的大家看看結合搬磚的例子來怎麼提升呢。