網站首頁 編程語言 正文
文章運行環境:go version go1.16.6 darwin/amd64
并發不安全
看下面的代碼,大家覺得會輸出什么?大多數人應該都會覺得輸出""、abc、neoj 這三種情況,但真實的情況并不是這樣,真實情況是只輸出 “” 空字符串。
結合日常的工作,類似這種并發操作同一個變量的情況也比較常見,為什么業務沒有發生異常問題?
var name string = "" func main() { go func() { for { name = "abc" } }() go func() { for { name = "neoj" } }() for { fmt.Println(name) } }
1.14 之后引入了 G 搶占式調度,那為什么代碼中的兩個協程沒有執行呢?其實是編譯器做了優化,這兩個協程被省略掉了。
我們對代碼做一點調整,在協程中加一行空的輸出,輸出結果中出現了一些特例,比如:neo、abca。其中,neo 字符串長度等于 abc 的長度,而 abca 的長度等于 neoj 的長度。
var name string = "" func main() { go func() { for { name = "abc" fmt.Printf("") } }() go func() { for { name = "neoj" fmt.Printf("") } }() for { if name != "abc" && name != "neoj" { fmt.Println(name) } } }
例子說明,string 的賦值并不是原子的。
Go 語言中 string 的內存結果如下,它包含兩部分:Data 表示實際的數據部分,而 Len 表示字符串的長度。
所以,通過方法 len 來計算字符串的長度并不會有性能開銷,len 方法會直接返回結構體的 Len 屬性;而傳遞字符串類型的參數,使用指針類型和值類型,性能上也不會有太大差別。
type StringHeader struct { Data uintptr Len int }
字符串的并發不安全,主要就是給這兩個字段的賦值,沒有辦法保證原子性。參考 runtime/string.go 中的源碼,我們可以了解字符串生成過程。
并發賦值的情況下,Data 指向的地址和 Len 無法保證一一對應。所以,通過 Data 獲取到內存的首地址,通過 Len 去讀取指定長度的內存時,就會出現內存讀取異常的情況。
func rawstring(size int) (s string, b []byte) { p := mallocgc(uintptr(size), nil, false) stringStructOf(&s).str = p stringStructOf(&s).len = size *(*slice)(unsafe.Pointer(&b)) = slice{p, size, size} return }
rawstring 函數在字符串拼接的時候被調用,我們代碼中創建一個字符串類型,每次都生成一份新的內存空間。特別強調,創建和字符串賦值需要區分開來。賦值的過程其實是值拷貝,拷貝的便是 StringHeader 結構體。
var name string = "" func main() { blog := name fmt.Println(blog) }
上面的變量 blog 是 name 的值拷貝,底層指向的字符串是同一塊內存空間。這個賦值過程中,發生拷貝的只是外層的 StringHeader 對象。
Go 中通過 unsafe 包可以強制對內存數據做類型轉換,我們將 blog 和 name 的內存地址打印出來比較一下。最終打印輸出兩個變量的地址和Data地址。可以看出,賦值前后,Data指向的地址并沒有發生變化。
type StringHeader struct { Data uintptr Len int } var name string = "g" func main() { blog := name n := (*StringHeader)(unsafe.Pointer(&name)) b := (*StringHeader)(unsafe.Pointer(&blog)) fmt.Println(&n, n.Data) // 0xc00018a020 17594869 fmt.Println(&b, b.Data) // 0xc00018a028 17594869 }
string 并發不安全讀寫,會導致線上服務偶發 panic。比如使用 json 對內存異常的 string 做序列化的時候。下面的例子中,其中一個協程用來賦值為空,非常容易復現 panic。
type People struct { Name string } var p *People = new(People) func main() { go func() { for { p.Name = "" } }() go func() { for { p.Name = "neoj" } }() for { _, _ = json.Marshal(p) } }
下面是 panic 的堆棧信息,空字符串的 Data 指向的是 nil 的地址,而并發導致 Len 字段有值,最終導致發生 panic。
競態競爭
對同一個變量并發讀寫,如果沒有使用輔助的同步操作,就會出現不符合預期的情況。直白的講,我們開發完一個程序之后,針對同樣的輸入,會輸出什么結果,我們是不確定的。
可以參考 The Go Memory Model 的介紹,強調一下數據競爭的概念:
A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by the sync/atomic package
幸運的是,Go 已經集成了現成的工具來診斷數據競爭:-race
。在 go build、或者直接執行的時候,指定 -race
屬性,系統會做數據競爭檢測,并打印輸出。
以最近的代碼為例,如果你使用的也是 goland 編譯器,只需要在 Run Configurations / Go tool arguments 中指定 -race
屬性,運行程序,就會出現下面的檢測結果:
面對生產環境,-race
有比較嚴重的性能開銷,我們最好是開發環境做競態檢測。
-race
是通過編譯器注入代碼來執行檢測的,在函數執行前、執行后都會做內存統計。也就是說:只有被執行到的代碼才能被檢測到。所以,如果開發階段做競態檢測的話,一定要保證代碼被執行到了。
再加上埋點的內存統計也是有策略的,也不可能保證存在數據競爭的代碼就一定會被檢測出來,最好可以多執行幾次來避免這種情況。
字符串優化
因字符串并發讀寫導致的 panic,很容易被 Go 的字符串優化帶偏。
我在第一次遇到這種情況的時候,想到的居然是:會不會是底層優化導致的。因為發生 panic 的代碼用到了 map 的數據結構。這種想法很快被我用測試用例排除了。
[]byte 到 string 類型轉換是比較常規的操作,正常情況下,轉換都會申請了一份新的內存空間。但 Go 為了提高性能,在某些場景下 string 和 []byte 會共用一份內存空間,這種場景下也能寫亂內存。
// slicebytetostringtmp returns a "string" referring to the actual []byte bytes. // func slicebytetostringtmp(ptr *byte, n int) (str string) { if raceenabled && n > 0 { racereadrangepc(unsafe.Pointer(ptr), uintptr(n), getcallerpc(), funcPC(slicebytetostringtmp)) } if msanenabled && n > 0 { msanread(unsafe.Pointer(ptr), uintptr(n)) } stringStructOf(&str).str = unsafe.Pointer(ptr) stringStructOf(&str).len = n return }
程序中出現問題,還是要先充分審查自己開發的代碼
原文鏈接:https://blog.csdn.net/whynottrythis/article/details/128600337
相關推薦
- 2023-04-01 C++11中強類型枚舉的使用_C 語言
- 2022-11-23 詳解React?Native中如何使用自定義的引用路徑_React
- 2022-08-14 Hyper-V設置虛擬機固定Ip的方法步驟_Hyper-V
- 2022-10-06 Go語言實現常用排序算法的示例代碼_Golang
- 2022-05-11 兩分鐘完成創建virtualbox創建k8s集群詳解
- 2022-07-22 vi編輯器設置自定義快捷鍵自動生成c語言的main函數
- 2022-09-18 Python中np.linalg.norm()用法實例總結_python
- 2023-10-26 解決:NODE_ENV 不是內部或外部命令,也不是可運行的程序,或者批處理文件
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支