1. 程式人生 > >go--->共享內存和通信兩種並發模式原理探究

go--->共享內存和通信兩種並發模式原理探究

表現 cond 原理 second chan listen 今天 想象 unlock

共享內存和通信兩種並發模式原理探究

並發理解
  • 人類發明計算機編程的本質目的是為了什麽呢?毫無疑問是為了解決人類社會中的各種負責業務場景問題。ok,有了這個出發點,那麽想象一下,比如你既可以一心一意只做一件事,你也可以同時做多件事,比如,你計劃今天上午計劃就是看足球比賽,ok,你今天的工作就是串行的,單進程的,你只需要完成一件事。但是不巧呢,你媽媽說讓你幫她切肉,你媽媽上午要出門有點事,同時不巧呢,你老婆說她上午也要出門,讓你幫著打掃家裏衛生,這時你今天就要同時做三件事,看比賽,切肉,打掃衛生。這時你的場景應該就是看比賽,看一會暫停去,打掃一下衛生,或者去切肉,切累了去看會繼續去看比賽,就這樣,上午把事情都做完了。
  • 程序在運行一個進程事,會有自己的調用棧和堆,還有一個完成的上下文,而cpu在並發調度多個進程時就是會有多個調用棧和堆,多個上下文,每個進程只有在獲取cpu調用的時間段時,才能回復這個進程的上下文繼續執行,否則就會保存進程的山下文。如果不理解就可以把上邊的你當作一個單核的cpu,比賽,切肉,和打掃衛生三件事,哪件事獲取了你的時間段,它才會繼續下去,否則就會以一種狀態保存下來的形式暫停下來。
  • 如果你的計算機是雙核的,那就相當於你叫了一個哥們來幫你一起做這三件事,他切肉,你打掃衛生,事情會更快的結束,計算機也是這樣,由原來的多個進程等待一個cpu的時間段,變成多個進程等待兩個cpu的時間段,只要進程獲取任一個cpu的時間段就可以繼續執行

    並發優勢
  • 由上文我們就可以理解並發的優勢
    • 更能客觀題表現實際現實問題的模型
    • 更能充分利用cpu的核心優勢
    • 更高效地解決問題

      並發關鍵詞
  • 進程(或者線程/協程)
  • cpu時間段
  • 如果多個進程之間在業務邏輯上有聯系,那麽就會涉及到的關鍵詞就是:關鍵資源使用權的分配

    並發案例
  • 現在有一個銀行,銀行裏有一個賬戶,賬號對應一張卡,通過這張卡可以對其賬戶進行存款,取款,查詢操作。這個業務模型是單進程的

//賬戶先抽象出一層接口
type Account interface {
  Withdraw(uint)
  Deposit(uint)
  Balance() int
}
// 銀行類和方法
type Bank struct {
  account Account   //銀行裏有一個賬戶
}

func NewBank(account Account) *Bank {
  return &Bank{account: account}
}

func (bank *Bank) Withdraw(amount uint, actor_name string) {
  fmt.Println("[-]", amount, actor_name)
  bank.account.Withdraw(amount)
}

func (bank *Bank) Deposit(amount uint, actor_name string) {
  fmt.Println("[+]", amount, actor_name)
  bank.account.Deposit(amount)
}

func (bank *Bank) Balance() int {
  return bank.account.Balance()
}

//實現Account接口
ype SimpleAccount struct{
  balance int
}

func NewSimpleAccount(balance int) *SimpleAccount {
  return &SimpleAccount{balance: balance}
}

func (acc *SimpleAccount) Deposit(amount uint) {
  acc.setBalance(acc.balance + int(amount))
}

func (acc *SimpleAccount) Withdraw(amount uint) {
  if acc.balance >= int(amount) {
    acc.setBalance(acc.balance - int(amount))
  } else {
    panic("傑克窮死")
  }
}

func (acc *SimpleAccount) Balance() int {
  return acc.balance
}

func (acc *SimpleAccount) setBalance(balance int) {
  acc.add_some_latency()  //關鍵:增加一個延時函數,方便演示
  acc.balance = balance
}

func (acc *SimpleAccount) add_some_latency() {
  <-time.After(time.Duration(rand.Intn(100)) * time.Millisecond)
}


//主函數調用
func main() {
  balance := 80
  b := NewBank(NewSimpleAccount(balance))
  
  fmt.Println("初始化余額", b.Balance())
  
  b.Withdraw(30, "馬伊琍")
  
  fmt.Println("-----------------")
  fmt.Println("剩余余額", b.Balance())
}

//結果
初始化余額 80
[-] 30 馬伊琍
-----------------
剩余余額 50
  • 上邊是一個賬戶對應一張卡,但是現實中,一個賬戶會有多個附屬卡,這些卡都可以對賬戶進行操作,當多個卡同時對一個賬戶進行操作時就有可能出現問題:當一張卡A要對賬戶取錢,這時A先從賬戶中查詢出錢80,然後減去要取走的錢30,最有將減後的結果寫入賬戶,不巧的是同時有一張卡B也要取錢,在A還沒有將減後的結果50寫入賬戶之前,也從賬戶中查詢處理總賬的錢80,此時錢還沒有改變,然後B也做同樣的操作,即用總賬減去取走的錢10,然後將結果70寫入賬戶,如果A此時已經完成了修改寫入操作,此時結果是50,但B還是將70寫入了賬戶,這時賬戶的最終結果是70,而業務是實際正確結果應該是40,所以就有問題,代碼如下
func main() {
  balance := 80
  b := NewBank(NewSimpleAccount(balance))
  fmt.Println("初始化余額", b.Balance())
  done := make(chan bool)
  go func() { b.Withdraw(30, "馬伊琍"); done <- true }()
  go func() { b.Withdraw(10, "姚笛"); done <- true }()
  
  //等待 goroutine 執行完成
  <-done
  <-done
  
  fmt.Println("-----------------")
  fmt.Println("剩余余額", b.Balance())
}


//結果
初始化余額 80
[-] 30 馬伊琍
[-] 10 姚笛
-----------------
剩余余額 70
  • 所以上邊並發問題就要另想辦法解決。
並發模型
  • 什麽是並發模型?
    • 個人理解並發模型其實就是前輩再解決並發問題後的經驗積累的基礎上對不同問題模型的不同處理方式的一種總結。
    • 我們直到並發的本質是解決如果更合理地去分配cpu的時間段,那麽實際業務中,不同的場景cpu的時間段的分配算法也是有所差異。所以並發模型的本質就是合理分配cpu時間段和關鍵內存資源的不同算法
  • 並發模型分類(並發模型較多,本文只分析以下兩種)
    • 共享內存模式
    • 通信模式
共享內存模式
  • 共享內存模式理解
    • 這裏舉一個不太雅但很形象的例子:比如現在多個人去一個洗手間上大號,不巧呢洗手間只有一個坑位。這時呢沒辦法,只能是一個人進去,關上門,告訴其他人坑位使用中,其他人只能等待,等這個人問題解決完了,打開門,走人,下一個人進去,關門解決問題。
    • 上邊多個人可以理解為多個進程,坑位可以理解為關鍵資源,上邊問題之所以可以有序解決還有一個關鍵東西就是那個門。門一旦鎖住,後邊的人只能等待。
    • 借助以上例子我們就可以理解共享內存模式本質就是:當多個進程需要使用關鍵資源時,那麽將關鍵資源加上排他鎖,哪個進程開始使用資源,就將資源鎖住,不允許其他進程使用這塊資源,如果有其他進程想使用這塊進程,那它就只能等待前一個進程將自己的問題處理完,將資源的使用權釋放,下一個進程才能使用,同時下一個進程開始使用時也會先給資源加鎖。
  • 用共享內存模式解決上邊問題
    • 要解決上邊問題,那麽核心邏輯就是要實現每個卡在操作賬戶時要先鎖定賬戶這個核心資源,其他進程來操作這個賬戶時要等待上一個進程釋放賬戶的使用權後,才能去獲取賬戶的使用權去操作。所以應該給賬戶擴展加鎖,解鎖的功能,所以我們在上邊簡單賬戶的基礎上做擴展。
type LockingAccount struct {
  lock    sync.Mutex //關於鎖的擴展
  account *SimpleAccount   //繼承
}

//封裝一下 SimpleAccount
func NewLockingAccount(balance int) *LockingAccount {
  return &LockingAccount{account: NewSimpleAccount(balance)}
}

func (acc *LockingAccount) Deposit(amount uint) {
  acc.lock.Lock()
  defer acc.lock.Unlock()
  acc.account.Deposit(amount)
}

func (acc *LockingAccount) Withdraw(amount uint) {
  acc.lock.Lock()
  defer acc.lock.Unlock()
  acc.account.Withdraw(amount)
}

func (acc *LockingAccount) Balance() int {
  acc.lock.Lock()
  defer acc.lock.Unlock()
  return acc.account.Balance()
}


//主函數調用
func main() {
  balance := 80
  b := NewBank(NewLockingAccount(balance))
  
  fmt.Println("初始化余額", b.Balance())
  
  done := make(chan bool)
  
  go func() { b.Withdraw(30, "馬伊琍"); done <- true }()
  go func() { b.Withdraw(10, "姚笛"); done <- true }()
  
  //等待 goroutine 執行完成
  <-done
  <-done
  
  fmt.Println("-----------------")
  fmt.Println("剩余余額", b.Balance())
}


//結果
初始化余額 80
[-] 30 馬伊琍
[-] 10 姚笛
-----------------
剩余余額 40


//實際流程
                ________________
                _馬伊琍_|__姚笛__
加鎖                   ><
得到余額            80  |
取錢               -30  |
當前余額            50  |
                   ... |
設置余額            50  |
解除鎖                 <>
                       |
當前余額                50
                       |
加鎖                   ><
得到余額                |  50
取錢                    | -10
當前余額                |  40
                       |  ...
設置余額                |  40
解除鎖                  <>
                ________________
剩余余額                40
  • 如果對mysql比較數的人,應該會想到,innodb的事務是基於行鎖實現的,行鎖其實就是一個排他鎖,其並發模型本質就是共享內存模式。
通信模式
  • 舉例理解
    • 假如現在有三個人分別拿著同一個賬戶的不同卡ABC,卡A和卡B要去銀行對賬戶進行取錢操作,卡c要去銀行進行查詢賬戶操作,一般的銀行我們知道是有不同的業務窗口,不同的窗口,假如現在只有三個窗口即取錢窗口,存錢窗口,還有查詢窗口。同時我們也知道銀行都有一個叫號機,我們去銀行辦業務,要先去叫號機面前告訴叫號機我們要辦理的業務類型,叫號機會給你一個排隊號,當你拿到排隊號之後,你就只能去等待。
    • 上邊三個人拿著三張卡對應的就是多進程,人和叫號機,叫號機和業務窗口之間的通信都是通過小紙條,即消息來實現。而這裏的消息對應就是go中的channel,叫號機可以暫且理解成go中的關鍵字select。
    • 實際go的程序執行邏輯和上邊現實中銀行的業務模型還是有點差異,其實際執行流程是這樣的:卡A和卡B要去辦理同一種業務取錢,那麽他們要先建立自己的消息(channel),消息裏包含了自己要辦理業務的類型(取錢)和相關數據(30,10),(然後將自己的消息交給select【可以理解成叫號機】,其實就是select中的case),然後卡A卡B分別阻塞,去等待結果;當select一旦檢測到有人給自己註冊了消息,然後就會馬上取出一個卡A的消息(假如是卡A的消息先被檢測到),同時通知相應取錢業務窗口去處理卡A的取錢操作,卡B則繼續阻塞等待,到取錢業務窗口完成卡A的取錢操作後,select則會去取下一條發送過來的消息(channel)。同時在select取出卡A的消息後,卡A會馬上得到通知,然後不再阻塞繼續執行,完成後續操作後銷毀釋放系統資源。
    • 再來說一下卡C的執行流程,卡C是要查詢賬戶總額,那麽它就要先建立並註冊自己的channel(消息),但是卡C的業務有一個特殊點就是,它要獲取一個反饋結果,即其註冊給select的消息還要考慮到業務窗口完成後將查詢結果以消息的方式反饋回來,所以卡C的消息結構就需要channel裏邊再嵌套一個channel,外層的channel為了實現讓叫號機(select)可以發通知給業務窗口,裏邊的channel則是為了讓業務窗口處理完業務後將結果放進去,從而讓卡C可以再後續操作中拿到業務窗口反饋的消息結果。所以從本質上來講,卡C的業務是有兩個阻塞點的。
    • 在上邊的過程中要註意select是相繼執行的,即只有一個case執行完成後,它才會去遍歷取下一個消息。
  • 利用通信模式處理上邊業務
package main

import (
    "fmt"
    "time"
    "math/rand"
)
// 主函數調用
func main() {
    balance:=80
    b:=NewBank(NewConcurrentAccount(uint8(balance)))
    fmt.Println("初始化余額", b.Balance())
    donechan:=make(chan bool)
    go func() {
        b.Withdraw(uint8(30),"daughter")
        donechan<-true
    }()
    go func() {
        b.Withdraw(uint8(10),"son")
        donechan<-true
    }()
    <-donechan
    <-donechan
    fmt.Println("________________")
    fmt.Println("剩余錢",b.Balance())
}

type Account interface {
    Deposit(uint82 uint8)  //存錢
    Withdraw(uint82 uint8) //取錢
    Balance()uint8  //查看錢
}
type Bank struct {
    account Account
}

func NewBank(account Account)*Bank  {
    return &Bank{account:account}
}
func (bank *Bank)Deposit(amount uint8,name string)  {
    fmt.Println("[+]",amount,name)
    bank.account.Deposit(amount)
}
func (bank *Bank)Withdraw(amount uint8,name string)  {
    fmt.Println("[-]",amount,name)
    bank.account.Withdraw(amount)
}
func (bank *Bank)Balance()uint8  {
    return bank.account.Balance()
}
type SimepleAccount struct {
    balance uint8
}

func NewSimepleAccount(balance uint8)*SimepleAccount  {
    return &SimepleAccount{balance:balance}
}
func (account *SimepleAccount)setBalance(balance uint8)  {
    account.add_some_latency()
    account.balance=balance
}
func (account *SimepleAccount) add_some_latency() {
    <-time.After(time.Duration(rand.Intn(100)) * time.Millisecond)
}
func (account *SimepleAccount)Deposit(amount uint8)  {
    account.setBalance(account.balance+amount)
}
func (account *SimepleAccount)Withdraw(amount uint8)  {
    account.setBalance(account.balance-amount)
}
func (account *SimepleAccount)Balance() uint8 {
    return account.balance
}
//擴展SimpleAccount,增加通信功能
type ConcurrentAccount struct {
    account *SimepleAccount
    deposit chan uint8
    withdraw chan uint8
    balance chan chan uint8
}

func NewConcurrentAccount(amount uint8)*ConcurrentAccount  {
    acc:=&ConcurrentAccount{
        account:NewSimepleAccount(amount),
        deposit:make(chan uint8),
        withdraw:make(chan uint8),
        balance:make(chan chan uint8),
    }
    acc.listen()
    return acc
}
func (account *ConcurrentAccount)Balance() uint8  {
    ch:=make(chan uint8)
    account.balance<-ch  //第一層阻塞
    return <-ch //第二層阻塞
}
func (account *ConcurrentAccount)Withdraw(amount uint8)  {
    account.withdraw<- amount
}
func (account *ConcurrentAccount)Deposit(amount uint8)  {
    account.deposit<- amount
}
func (account *ConcurrentAccount)listen()  {
    go func() {
        for {
            select {  //叫號機,每一個case都是註冊的消息
            case amt:=<- account.deposit:  
                account.account.Deposit(amt)
            case amt:=<-account.withdraw:
                account.account.Withdraw(amt)
            case ch:=<-account.balance:
                ch<-account.account.Balance()  
            }
        }
    }()
}
  • 要從本質理解透徹channel原理,則需要去理解unix的管道機制(簡單理解,後續會去專門系統分析其原理),因為channel就是借鑒unix的管道機制實現的。
    • 管道是由內核管理的一個緩沖區,相當於我們放入內存中的一個紙條。一個進程在管道的尾部寫入數據,另一個進程從管道的頭部讀出數據。管道包括無名管道和有名管道兩種,前者只能用於父進程和子進程間的通信,後者可用於運行於同一系統中的任意兩個進程間的通信。下面用一個示意圖來表示:
      技術分享圖片
  • 管道通信的特點
    • 管道通訊是單向的,先進先出,即為隊列結構,有固定的讀端和寫端。
    • 數據被進程從管道讀出後,在管道中該數據就不存在了。
    • 當進程去讀取空管道的時候,進程會阻塞。
    • 當進程往滿管道寫數據時,進程會阻塞。
(ps:以上是本人對並發的粗淺理解,歡迎高手指點批評)

github源碼:[email protected]:Frankltf/concurrency_go.git

參考:http://ju.outofmemory.cn/entry/73895
https://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html
https://blog.csdn.net/u010853261/article/details/53464053

go--->共享內存和通信兩種並發模式原理探究