Go 語言的 append 不總是安全執行緒的

來源:互聯網
上載者:User
## 樣本問題我經常看到一些 bug 是由於沒有線上程安全下在 slice 上進行 append 而引起的。下面用單元測試來舉一個簡單的例子。這個測試有兩個協程對相同的 slice 進行 append 操作。如果你使用 `-race` flag 來執行這個單元測試,效果更好。```gopackage mainimport ("sync""testing")func TestAppend(t *testing.T) {x := []string{"start"}wg := sync.WaitGroup{}wg.Add(2)go func() {defer wg.Done()y := append(x, "hello", "world")t.Log(cap(y), len(y))}()go func() {defer wg.Done()z := append(x, "goodbye", "bob")t.Log(cap(z), len(z))}()wg.Wait()}```現在,讓我們稍微修改代碼,以給這個名為 `x` 的 slice 在建立是預留一些容量。唯一改動的地方是第 9 行。```gopackage mainimport ("testing""sync")func TestAppend(t *testing.T) {x := make([]string, 0, 6)wg := sync.WaitGroup{}wg.Add(2)go func() {defer wg.Done()y := append(x, "hello", "world")t.Log(len(y))}()go func() {defer wg.Done()z := append(x, "goodbye", "bob")t.Log(len(z))}()wg.Wait()}```如果我們執行這個測試時帶上 `-race` flag ,我們可以注意到一個競爭條件。```< go test -race .==================WARNING: DATA RACEWrite at 0x00c4200be060 by goroutine 8:_/tmp.TestAppend.func2()/tmp/main_test.go:20 +0xcbPrevious write at 0x00c4200be060 by goroutine 7:_/tmp.TestAppend.func1()/tmp/main_test.go:15 +0xcbGoroutine 8 (running) created at:_/tmp.TestAppend()/tmp/main_test.go:18 +0x14ftesting.tRunner()/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16dGoroutine 7 (running) created at:_/tmp.TestAppend()/tmp/main_test.go:13 +0x105testing.tRunner()/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16d====================================WARNING: DATA RACEWrite at 0x00c4200be070 by goroutine 8:_/tmp.TestAppend.func2()/tmp/main_test.go:20 +0x11aPrevious write at 0x00c4200be070 by goroutine 7:_/tmp.TestAppend.func1()/tmp/main_test.go:15 +0x11aGoroutine 8 (running) created at:_/tmp.TestAppend()/tmp/main_test.go:18 +0x14ftesting.tRunner()/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16dGoroutine 7 (finished) created at:_/tmp.TestAppend()/tmp/main_test.go:13 +0x105testing.tRunner()/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16d==================--- FAIL: TestAppend (0.00s)main_test.go:16: 2main_test.go:21: 2testing.go:730: race detected during execution of testFAILFAIL _/tmp 0.901s```## 解釋為什麼測試失敗理解為什麼這個失敗會發生,請看看這箇舊例子的 `x` 的記憶體布局![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-append-is-not-always-thread-safe/x-starts-with-no-capacity-to-change.png)x 沒有足夠的容量進行修改Go 語言發現沒有足夠的記憶體空間來儲存 `"hello", "world"` 和 `"goodbye", "bob"`,於是分配的新的記憶體給 `y` 與 `z`。資料競爭不會在多進程讀取記憶體時發生,`x` 沒有被修改。這裡沒有衝突,也就沒有競爭。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-append-is-not-always-thread-safe/z-and-y-get-their-own-memory.png)z 與 y 擷取新的記憶體空間在新的代碼裡,事情不一樣了![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-append-is-not-always-thread-safe/x-has-capacity-for-more.png)x 有更多的容量在這裡,go 注意到有足夠的記憶體存放 `“hello”, “world”`,另一個協程也發現有足夠的空間存放 `“goodbye”, “bob”`,這個競爭的發生是因為這兩個協程都嘗試往同一個記憶體空間寫入,誰也不知道誰是贏家。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-append-is-not-always-thread-safe/who-wins.png)誰贏了?這是 Go 語言的一個特性而非 bug ,`append` 不會強制每一次調用它都申請新的記憶體。它允許使用者在迴圈內進行 `append` 操作時不會破壞記憶體回收機制。缺點是你必須清楚知道在多個協程對 slice 的操作。## 這個 bug 的認知根源我相信這個 bug 存在是 Go 的為了儲存簡單,將許多概念放到 slice 中,在大多數開發人員中看到的思維過程是:1. `x=append(x, ...)` 看起來你要獲得一個新的 slice。2. 大多數傳回值的函數都不會改變它們的輸入。3. 我們使用 `append` 通常都是得到一個新的 slice。4. 錯誤地認為append是唯讀。## 認知這個 bug值得注意的是如果第一個被 `append` 的變數不是一個本地變數(譯者:本地變數,即變數與 append 在同一代碼塊)。這個 bug 通常發生在:進行 append 操作的變數存在一個結構體中,而這個結構體是通過函數傳參進來的。例如,一個結構體可以有預設值,可以被各個請求 append。小心對共用記憶體的變數進行 append ,或者這個記憶體空間(變數)並不是當前協程獨佔的。## 解決方案最簡單的解決方案是不使用共用狀態的第一個變數來進行 append 。相反,根據你的需要來 `make` 一個新的 `slice` ,使用這個新的 slice 作為 append 的第一個變數。下面是失敗的測試樣本的修正版,這裡的替代方法是使用 [copy](https://golang.org/pkg/builtin/#copy) 。```gopackage mainimport ("sync""testing")func TestAppend(t *testing.T) {x := make([]string, 0, 6)x = append(x, "start")wg := sync.WaitGroup{}wg.Add(2)go func() {defer wg.Done()y := make([]string, 0, len(x)+2)y = append(y, x...)y = append(y, "hello", "world")t.Log(cap(y), len(y), y[0])}()go func() {defer wg.Done()z := make([]string, 0, len(x)+2)z = append(z, x...)z = append(z, "goodbye", "bob")t.Log(cap(z), len(z), z[0])}()wg.Wait()}```對本地變數進行第一次的 append

via: https://medium.com/@cep21/gos-append-is-not-always-thread-safe-a3034db7975

作者:Jack Lindamood 譯者:lightfish-zhang 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

365 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.