介紹Go競爭檢測器

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

原文串連http://blog.golang.org/race-detector

介紹:

競爭條件是最狡詐的、最難以找到的編程錯誤。通常,在代碼被布置到生產環境很久以後,它們才會出現並且造成奇怪的、神秘的錯誤。儘管Go語言的並發機制使得更容易的編寫出乾淨的並發代碼,依然無法避免競爭條件的出現。小心、勤勉以及測試是必須的。工具也可以提供協助。

我們很高興的宣布Go1.1包含了一個競爭檢測器,一個全新的工具,用於在Go代碼中找到競爭條件。該工具當前在Linux,OS X 和Windows平台可用,只要CPU是64位的x86架構。

競爭檢測器基於C/C++的ThreadSanitizer 執行階段程式庫,該庫在Google內部代碼基地和Chromium找到許多錯誤。這個技術在2012年九月整合到Go中,從那時開始,它已經在標準庫中檢測到42個競爭條件。現在,它已經是我們持續構建過程的一部分,當競爭條件出現時,它會繼續捕捉到這些錯誤。


工作原理

競爭檢測器整合在go工具鏈中。當使用了-race作為命令列參數後,編譯器會插樁代碼,使得所有代碼在訪問記憶體時,會記錄訪問時間和方法。同時執行階段程式庫會觀察對共用變數的未同步訪問。當這種競爭行為被檢測到,就會輸出一個警告資訊。(查看這裡瞭解演算法的具體細節)

由於設計原因,競爭檢測器只有在被啟動並執行代碼觸發時,才能檢測到競爭條件,因此在現實的負載條件下運行是非常重要的。但是由於代碼插樁,程式會使用10倍的CPU和記憶體,所以總是運行插樁後的程式是不現實的。矛盾的解決方案之一就是使用插樁後的程式來運行測試。負載測試和整合測試是好的候選,因為它們傾向於檢驗代碼的並發部分。另外的方法是將單個插樁後的程式布置到運行伺服器組成的池中,並且給予生產環境的負載。

使用方法:

競爭檢測器已經完全整合到Go工具鏈中,僅僅添加-race標誌到命令列就使用了檢測器。

$ go test -race mypkg    // 測試包$ go run -race mysrc.go  // 編譯和運行程式$ go build -race mycmd   // 構建程式$ go install -race mypkg // 安裝程式

擷取以下樣本程式,就可以嘗試使用競爭檢測器

$ go get -race code.google.com/p/go.blog/support/racy$ racy
這裡有兩個例子,是由競爭檢測器捕捉到的而上報產生的話題。

第1個例子: Timer.Reset

第一個例子是一個由競爭檢測器發生的真實bug的簡化版。它使用一個計時器來隨機的時間間隔後列印訊息,該間隔在0-1秒之間。該過程重複5秒。代碼使用了time.AfterFunc來建立一個計時器,該計時器用於初次列印;隨後使用Reset方法來安排下一條訊息,每次都重用了該計時器。


 9 func main() {10     start := time.Now()11     var t *time.Timer12     t = time.AfterFunc(randomDuration(), func() {13         fmt.Println(time.Now().Sub(start)) 14         t.Reset(randomDuration())15     })16     time.Sleep(5 * time.Second)17 }18 19 func randomDuration() time.Duration {20     return time.Duration(rand.Int63n(1e9))21 }

看上去代碼很合理,但是在特定約束下,程式以一種令人驚奇的方式失敗了。

panic: runtime error: invalid memory address or nil pointer dereference[signal 0xb code=0x1 addr=0x8 pc=0x41e38a]goroutine 4 [running]:time.stopTimer(0x8, 0x12fe6b35d9472d96)    src/pkg/runtime/ztime_linux_amd64.c:35 +0x25time.(*Timer).Reset(0x0, 0x4e5904f, 0x1)    src/pkg/time/sleep.go:81 +0x42main.func·001()    race.go:14 +0xe3created by time.goFunc    src/pkg/time/sleep.go:122 +0x48

發生了什麼事情? 運行插樁後的代碼將給予更深入的資訊。

==================WARNING: DATA RACERead by goroutine 5:  main.func·001()     race.go:14 +0x169Previous write by goroutine 1:  main.main()      race.go:15 +0x174Goroutine 5 (running) created at:  time.goFunc()      src/pkg/time/sleep.go:122 +0x56  timerproc()     src/pkg/runtime/ztime_linux_amd64.c:181 +0x189==================

競爭檢測器發現了問題:在不同的goroutine中,對t有未同步的讀、寫操作。如果最初的計時間隔非常小,那麼計時器函數有可能在main線程給t賦值前就調用,所以t.Reset就是在值為nil的t上調用。

為了修正這個競爭條件,更改代碼使得對t的讀寫只能從main線程發出。

 9 func main() {10     start := time.Now()11     reset := make(chan bool)12     var t *time.Timer13     t = time.AfterFunc(randomDuration(), func() {14         fmt.Println(time.Now().Sub(start))15         reset <- true16     })17     for time.Since(start) < 5*time.Second {18         <-reset19         t.Reset(randomDuration())20     }21 }

這裡main線程完全負責設定、重設定時器t,一個全新的channel以安全執行緒的方式通訊是否定時器需要重設

另一種更簡單但是效率比較低的方法就是避免重用定時器

第2個例子: ioutil.Discard

第二個例子更加微妙

ioutil包中的Discard對象實現了介面io.Writer,但是拋棄了所有寫入的資料。可以將其當做/dev/null:用於發送需要讀取但不想儲存的資料。該對象被廣泛使用於io.Copy(),目的是耗盡讀取端的資料,如:

io.Copy(ioutil.Discard, reader)

在2011年7年,Go團隊注意到這樣使用Discard比較低效:Copy函數每次調用時會分配一個大小為32K位元組的內部緩衝,當Discadrd作為參數時,這個緩衝就沒用了。我們認為這種將Copy和Discard組合使用,不應該這樣浪費。

解決方案很簡單。如果指定的寫入端實現了ReadFrom方法,那麼如下的Copy調用

io.Copy(writer, reader)

 將被託管給可能更高效的調用

writer.ReadFrom(reader)

我們添加了一個ReadFrom()方法到Discard的底層類型,該類型有一個在所有使用者之間共用的內部緩衝。我們知道在理論上,存在著競爭條件,但是所有寫入該緩衝的資料被拋棄,所以我們認為這沒有什麼大不了的。

當實現了競爭檢測器後,它立即檢測出這段代碼存在問題。我們再次認為這是多此一舉,決定將這個競爭條件定義為非真實的。為了避免在構建過程中的該誤判,我們實現了一個非競爭版本,該版本只在競爭檢測器運行時可用。

但是幾個月後,Brad遇到了一個奇怪的bug。在經過數日的調試後,他將問題定位於一個有ioutil.Discrd引發的真實競爭條件上。

這就是已經的競爭發生的代碼,Discard是一個devNull,在所有使用者間共用快取。

var blackHole [4096]byte // shared bufferfunc (devNull) ReadFrom(r io.Reader) (n int64, err error) {    readSize := 0    for {        readSize, err = r.Read(blackHole[:])        n += int64(readSize)        if err != nil {            if err == io.EOF {                return n, nil            }            return        }    }}

Brad的程式包含一個trackDigestReader類型,其中封裝了一個io.Reader,並且記錄了已經閱讀內容的hash digest

type trackDigestReader struct {    r io.Reader    h hash.Hash}func (t trackDigestReader) Read(p []byte) (n int, err error) {    n, err = t.r.Read(p)    t.h.Write(p[:n])    return}

該代碼可以用於在讀取檔案的同時,計算其SHA-1 雜湊值:F

tdr := trackDigestReader{r: file, h: sha1.New()}io.Copy(writer, tdr)fmt.Printf("File hash: %x", tdr.h.Sum(nil))

某些情況下,資料並無寫入,但是依然需要hash該檔案,所以使用了Discard

io.Copy(ioutil.Discard, tdr)

但是在這種情況下,blackHole緩衝已經不再是一個黑洞,它是一個合法的地方,用於在讀取到資料到寫入hash.Hash之間儲存資料。在多線程並發hash檔案時,每個線程共用blackHole緩衝,競爭條件就會在讀取和hash之間破壞資料。沒有錯誤或者panic發生,但是hash值出錯了。多麼令人不愉快!

func (t trackDigestReader) Read(p []byte) (n int, err error) {    // the buffer p is blackHole    n, err = t.r.Read(p)    // p可能被其餘線程破壞    //     t.h.Write(p[:n])    return}

這個bug最終被修正,方法是給予每個ioutil.Discard使用者一個獨一無二的內部緩衝,消除了在共用快取上的競爭條件。

結論

在檢測並發編程的正確性方面,競爭檢測器是一個威力強大的工具。它不會誤判,要認真對待其發出的警告。但是它依賴於你的測試,必須保證測試覆蓋了代碼的並發部分,這樣檢測器才能完成任務。

你還在等待什麼?今天就在你的代碼上運行"go test -race"


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.