99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Go 語(yǔ)言 競(jìng)爭(zhēng)條件

2023-03-14 16:57 更新

原文鏈接:https://gopl-zh.github.io/ch9/ch9-01.html


9.1. 競(jìng)爭(zhēng)條件

在一個(gè)線性(就是說(shuō)只有一個(gè)goroutine的)的程序中,程序的執(zhí)行順序只由程序的邏輯來(lái)決定。例如,我們有一段語(yǔ)句序列,第一個(gè)在第二個(gè)之前(廢話),以此類推。在有兩個(gè)或更多goroutine的程序中,每一個(gè)goroutine內(nèi)的語(yǔ)句也是按照既定的順序去執(zhí)行的,但是一般情況下我們沒(méi)法去知道分別位于兩個(gè)goroutine的事件x和y的執(zhí)行順序,x是在y之前還是之后還是同時(shí)發(fā)生是沒(méi)法判斷的。當(dāng)我們沒(méi)有辦法自信地確認(rèn)一個(gè)事件是在另一個(gè)事件的前面或者后面發(fā)生的話,就說(shuō)明x和y這兩個(gè)事件是并發(fā)的。

考慮一下,一個(gè)函數(shù)在線性程序中可以正確地工作。如果在并發(fā)的情況下,這個(gè)函數(shù)依然可以正確地工作的話,那么我們就說(shuō)這個(gè)函數(shù)是并發(fā)安全的,并發(fā)安全的函數(shù)不需要額外的同步工作。我們可以把這個(gè)概念概括為一個(gè)特定類型的一些方法和操作函數(shù),對(duì)于某個(gè)類型來(lái)說(shuō),如果其所有可訪問(wèn)的方法和操作都是并發(fā)安全的話,那么該類型便是并發(fā)安全的。

在一個(gè)程序中有非并發(fā)安全的類型的情況下,我們依然可以使這個(gè)程序并發(fā)安全。確實(shí),并發(fā)安全的類型是例外,而不是規(guī)則,所以只有當(dāng)文檔中明確地說(shuō)明了其是并發(fā)安全的情況下,你才可以并發(fā)地去訪問(wèn)它。我們會(huì)避免并發(fā)訪問(wèn)大多數(shù)的類型,無(wú)論是將變量局限在單一的一個(gè)goroutine內(nèi),還是用互斥條件維持更高級(jí)別的不變性,都是為了這個(gè)目的。我們會(huì)在本章中說(shuō)明這些術(shù)語(yǔ)。

相反,包級(jí)別的導(dǎo)出函數(shù)一般情況下都是并發(fā)安全的。由于package級(jí)的變量沒(méi)法被限制在單一的gorouine,所以修改這些變量“必須”使用互斥條件。

一個(gè)函數(shù)在并發(fā)調(diào)用時(shí)沒(méi)法工作的原因太多了,比如死鎖(deadlock)、活鎖(livelock)和餓死(resource starvation)。我們沒(méi)有空去討論所有的問(wèn)題,這里我們只聚焦在競(jìng)爭(zhēng)條件上。

競(jìng)爭(zhēng)條件指的是程序在多個(gè)goroutine交叉執(zhí)行操作時(shí),沒(méi)有給出正確的結(jié)果。競(jìng)爭(zhēng)條件是很惡劣的一種場(chǎng)景,因?yàn)檫@種問(wèn)題會(huì)一直潛伏在你的程序里,然后在非常少見(jiàn)的時(shí)候蹦出來(lái),或許只是會(huì)在很大的負(fù)載時(shí)才會(huì)發(fā)生,又或許是會(huì)在使用了某一個(gè)編譯器、某一種平臺(tái)或者某一種架構(gòu)的時(shí)候才會(huì)出現(xiàn)。這些使得競(jìng)爭(zhēng)條件帶來(lái)的問(wèn)題非常難以復(fù)現(xiàn)而且難以分析診斷。

傳統(tǒng)上經(jīng)常用經(jīng)濟(jì)損失來(lái)為競(jìng)爭(zhēng)條件做比喻,所以我們來(lái)看一個(gè)簡(jiǎn)單的銀行賬戶程序。

// Package bank implements a bank with only one account.
package bank
var balance int
func Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }

(當(dāng)然我們也可以把Deposit存款函數(shù)寫(xiě)成balance += amount,這種形式也是等價(jià)的,不過(guò)長(zhǎng)一些的形式解釋起來(lái)更方便一些。)

對(duì)于這個(gè)簡(jiǎn)單的程序而言,我們一眼就能看出,以任意順序調(diào)用函數(shù)Deposit和Balance都會(huì)得到正確的結(jié)果。也就是說(shuō),Balance函數(shù)會(huì)給出之前的所有存入的額度之和。然而,當(dāng)我們并發(fā)地而不是順序地調(diào)用這些函數(shù)的話,Balance就再也沒(méi)辦法保證結(jié)果正確了??紤]一下下面的兩個(gè)goroutine,其代表了一個(gè)銀行聯(lián)合賬戶的兩筆交易:

// Alice:
go func() {
    bank.Deposit(200)                // A1
    fmt.Println("=", bank.Balance()) // A2
}()

// Bob:
go bank.Deposit(100)                 // B

Alice存了$200,然后檢查她的余額,同時(shí)Bob存了$100。因?yàn)锳1和A2是和B并發(fā)執(zhí)行的,我們沒(méi)法預(yù)測(cè)他們發(fā)生的先后順序。直觀地來(lái)看的話,我們會(huì)認(rèn)為其執(zhí)行順序只有三種可能性:“Alice先”,“Bob先”以及“Alice/Bob/Alice”交錯(cuò)執(zhí)行。下面的表格會(huì)展示經(jīng)過(guò)每一步驟后balance變量的值。引號(hào)里的字符串表示余額單。

Alice first        Bob first        Alice/Bob/Alice
          0                0                      0
  A1    200        B     100             A1     200
  A2 "= 200"       A1    300             B      300
  B     300        A2 "= 300"            A2  "= 300"

所有情況下最終的余額都是$300。唯一的變數(shù)是Alice的余額單是否包含了Bob交易,不過(guò)無(wú)論怎么著客戶都不會(huì)在意。

但是事實(shí)是上面的直覺(jué)推斷是錯(cuò)誤的。第四種可能的結(jié)果是事實(shí)存在的,這種情況下Bob的存款會(huì)在Alice存款操作中間,在余額被讀到(balance + amount)之后,在余額被更新之前(balance = ...),這樣會(huì)導(dǎo)致Bob的交易丟失。而這是因?yàn)锳lice的存款操作A1實(shí)際上是兩個(gè)操作的一個(gè)序列,讀取然后寫(xiě);可以稱之為A1r和A1w。下面是交叉時(shí)產(chǎn)生的問(wèn)題:

Data race
0
A1r      0     ... = balance + amount
B      100
A1w    200     balance = ...
A2  "= 200"

在A1r之后,balance + amount會(huì)被計(jì)算為200,所以這是A1w會(huì)寫(xiě)入的值,并不受其它存款操作的干預(yù)。最終的余額是$200。銀行的賬戶上的資產(chǎn)比Bob實(shí)際的資產(chǎn)多了$100。(譯注:因?yàn)閬G失了Bob的存款操作,所以其實(shí)是說(shuō)Bob的錢丟了。)

這個(gè)程序包含了一個(gè)特定的競(jìng)爭(zhēng)條件,叫作數(shù)據(jù)競(jìng)爭(zhēng)。無(wú)論任何時(shí)候,只要有兩個(gè)goroutine并發(fā)訪問(wèn)同一變量,且至少其中的一個(gè)是寫(xiě)操作的時(shí)候就會(huì)發(fā)生數(shù)據(jù)競(jìng)爭(zhēng)。

如果數(shù)據(jù)競(jìng)爭(zhēng)的對(duì)象是一個(gè)比一個(gè)機(jī)器字(譯注:32位機(jī)器上一個(gè)字=4個(gè)字節(jié))更大的類型時(shí),事情就變得更麻煩了,比如interface,string或者slice類型都是如此。下面的代碼會(huì)并發(fā)地更新兩個(gè)不同長(zhǎng)度的slice:

var x []int
go func() { x = make([]int, 10) }()
go func() { x = make([]int, 1000000) }()
x[999999] = 1 // NOTE: undefined behavior; memory corruption possible!

最后一個(gè)語(yǔ)句中的x的值是未定義的;其可能是nil,或者也可能是一個(gè)長(zhǎng)度為10的slice,也可能是一個(gè)長(zhǎng)度為1,000,000的slice。但是回憶一下slice的三個(gè)組成部分:指針(pointer)、長(zhǎng)度(length)和容量(capacity)。如果指針是從第一個(gè)make調(diào)用來(lái),而長(zhǎng)度從第二個(gè)make來(lái),x就變成了一個(gè)混合體,一個(gè)自稱長(zhǎng)度為1,000,000但實(shí)際上內(nèi)部只有10個(gè)元素的slice。這樣導(dǎo)致的結(jié)果是存儲(chǔ)999,999元素的位置會(huì)碰撞一個(gè)遙遠(yuǎn)的內(nèi)存位置,這種情況下難以對(duì)值進(jìn)行預(yù)測(cè),而且debug也會(huì)變成噩夢(mèng)。這種語(yǔ)義雷區(qū)被稱為未定義行為,對(duì)C程序員來(lái)說(shuō)應(yīng)該很熟悉;幸運(yùn)的是在Go語(yǔ)言里造成的麻煩要比C里小得多。

盡管并發(fā)程序的概念讓我們知道并發(fā)并不是簡(jiǎn)單的語(yǔ)句交叉執(zhí)行。我們將會(huì)在9.4節(jié)中看到,數(shù)據(jù)競(jìng)爭(zhēng)可能會(huì)有奇怪的結(jié)果。許多程序員,甚至一些非常聰明的人也還是會(huì)偶爾提出一些理由來(lái)允許數(shù)據(jù)競(jìng)爭(zhēng),比如:“互斥條件代價(jià)太高”,“這個(gè)邏輯只是用來(lái)做logging”,“我不介意丟失一些消息”等等。因?yàn)樵谒麄兊木幾g器或者平臺(tái)上很少遇到問(wèn)題,可能給了他們錯(cuò)誤的信心。一個(gè)好的經(jīng)驗(yàn)法則是根本就沒(méi)有什么所謂的良性數(shù)據(jù)競(jìng)爭(zhēng)。所以我們一定要避免數(shù)據(jù)競(jìng)爭(zhēng),那么在我們的程序中要如何做到呢?

我們來(lái)重復(fù)一下數(shù)據(jù)競(jìng)爭(zhēng)的定義,因?yàn)閷?shí)在太重要了:數(shù)據(jù)競(jìng)爭(zhēng)會(huì)在兩個(gè)以上的goroutine并發(fā)訪問(wèn)相同的變量且至少其中一個(gè)為寫(xiě)操作時(shí)發(fā)生。根據(jù)上述定義,有三種方式可以避免數(shù)據(jù)競(jìng)爭(zhēng):

第一種方法是不要去寫(xiě)變量??紤]一下下面的map,會(huì)被“懶”填充,也就是說(shuō)在每個(gè)key被第一次請(qǐng)求到的時(shí)候才會(huì)去填值。如果Icon是被順序調(diào)用的話,這個(gè)程序會(huì)工作很正常,但如果Icon被并發(fā)調(diào)用,那么對(duì)于這個(gè)map來(lái)說(shuō)就會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)。

var icons = make(map[string]image.Image)
func loadIcon(name string) image.Image

// NOTE: not concurrency-safe!
func Icon(name string) image.Image {
    icon, ok := icons[name]
    if !ok {
        icon = loadIcon(name)
        icons[name] = icon
    }
    return icon
}

反之,如果我們?cè)趧?chuàng)建goroutine之前的初始化階段,就初始化了map中的所有條目并且再也不去修改它們,那么任意數(shù)量的goroutine并發(fā)訪問(wèn)Icon都是安全的,因?yàn)槊恳粋€(gè)goroutine都只是去讀取而已。

var icons = map[string]image.Image{
    "spades.png":   loadIcon("spades.png"),
    "hearts.png":   loadIcon("hearts.png"),
    "diamonds.png": loadIcon("diamonds.png"),
    "clubs.png":    loadIcon("clubs.png"),
}

// Concurrency-safe.
func Icon(name string) image.Image { return icons[name] }

上面的例子里icons變量在包初始化階段就已經(jīng)被賦值了,包的初始化是在程序main函數(shù)開(kāi)始執(zhí)行之前就完成了的。只要初始化完成了,icons就再也不會(huì)被修改。數(shù)據(jù)結(jié)構(gòu)如果從不被修改或是不變量則是并發(fā)安全的,無(wú)需進(jìn)行同步。不過(guò)顯然,如果update操作是必要的,我們就沒(méi)法用這種方法,比如說(shuō)銀行賬戶。

第二種避免數(shù)據(jù)競(jìng)爭(zhēng)的方法是,避免從多個(gè)goroutine訪問(wèn)變量。這也是前一章中大多數(shù)程序所采用的方法。例如前面的并發(fā)web爬蟲(chóng)(§8.6)的main goroutine是唯一一個(gè)能夠訪問(wèn)seen map的goroutine,而聊天服務(wù)器(§8.10)中的broadcaster goroutine是唯一一個(gè)能夠訪問(wèn)clients map的goroutine。這些變量都被限定在了一個(gè)單獨(dú)的goroutine中。

由于其它的goroutine不能夠直接訪問(wèn)變量,它們只能使用一個(gè)channel來(lái)發(fā)送請(qǐng)求給指定的goroutine來(lái)查詢更新變量。這也就是Go的口頭禪“不要使用共享數(shù)據(jù)來(lái)通信;使用通信來(lái)共享數(shù)據(jù)”。一個(gè)提供對(duì)一個(gè)指定的變量通過(guò)channel來(lái)請(qǐng)求的goroutine叫做這個(gè)變量的monitor(監(jiān)控)goroutine。例如broadcaster goroutine會(huì)監(jiān)控clients map的全部訪問(wèn)。

下面是一個(gè)重寫(xiě)了的銀行的例子,這個(gè)例子中balance變量被限制在了monitor goroutine中,名為teller:

gopl.io/ch9/bank1

// Package bank provides a concurrency-safe bank with one account.
package bank

var deposits = make(chan int) // send amount to deposit
var balances = make(chan int) // receive balance

func Deposit(amount int) { deposits <- amount }
func Balance() int       { return <-balances }

func teller() {
    var balance int // balance is confined to teller goroutine
    for {
        select {
        case amount := <-deposits:
            balance += amount
        case balances <- balance:
        }
    }
}

func init() {
    go teller() // start the monitor goroutine
}

即使當(dāng)一個(gè)變量無(wú)法在其整個(gè)生命周期內(nèi)被綁定到一個(gè)獨(dú)立的goroutine,綁定依然是并發(fā)問(wèn)題的一個(gè)解決方案。例如在一條流水線上的goroutine之間共享變量是很普遍的行為,在這兩者間會(huì)通過(guò)channel來(lái)傳輸?shù)刂沸畔?。如果流水線的每一個(gè)階段都能夠避免在將變量傳送到下一階段后再去訪問(wèn)它,那么對(duì)這個(gè)變量的所有訪問(wèn)就是線性的。其效果是變量會(huì)被綁定到流水線的一個(gè)階段,傳送完之后被綁定到下一個(gè),以此類推。這種規(guī)則有時(shí)被稱為串行綁定。

下面的例子中,Cakes會(huì)被嚴(yán)格地順序訪問(wèn),先是baker gorouine,然后是icer gorouine:

type Cake struct{ state string }

func baker(cooked chan<- *Cake) {
    for {
        cake := new(Cake)
        cake.state = "cooked"
        cooked <- cake // baker never touches this cake again
    }
}

func icer(iced chan<- *Cake, cooked <-chan *Cake) {
    for cake := range cooked {
        cake.state = "iced"
        iced <- cake // icer never touches this cake again
    }
}

第三種避免數(shù)據(jù)競(jìng)爭(zhēng)的方法是允許很多goroutine去訪問(wèn)變量,但是在同一個(gè)時(shí)刻最多只有一個(gè)goroutine在訪問(wèn)。這種方式被稱為“互斥”,在下一節(jié)來(lái)討論這個(gè)主題。

練習(xí) 9.1: 給gopl.io/ch9/bank1程序添加一個(gè)Withdraw(amount int)取款函數(shù)。其返回結(jié)果應(yīng)該要表明事務(wù)是成功了還是因?yàn)闆](méi)有足夠資金失敗了。這條消息會(huì)被發(fā)送給monitor的goroutine,且消息需要包含取款的額度和一個(gè)新的channel,這個(gè)新channel會(huì)被monitor goroutine來(lái)把boolean結(jié)果發(fā)回給Withdraw。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)