網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
文章運(yùn)行環(huán)境:go version go1.16.6 darwin/amd64
并發(fā)不安全
看下面的代碼,大家覺(jué)得會(huì)輸出什么?大多數(shù)人應(yīng)該都會(huì)覺(jué)得輸出""、abc、neoj 這三種情況,但真實(shí)的情況并不是這樣,真實(shí)情況是只輸出 “” 空字符串。
結(jié)合日常的工作,類似這種并發(fā)操作同一個(gè)變量的情況也比較常見(jiàn),為什么業(yè)務(wù)沒(méi)有發(fā)生異常問(wèn)題?
var name string = "" func main() { go func() { for { name = "abc" } }() go func() { for { name = "neoj" } }() for { fmt.Println(name) } }
1.14 之后引入了 G 搶占式調(diào)度,那為什么代碼中的兩個(gè)協(xié)程沒(méi)有執(zhí)行呢?其實(shí)是編譯器做了優(yōu)化,這兩個(gè)協(xié)程被省略掉了。
我們對(duì)代碼做一點(diǎn)調(diào)整,在協(xié)程中加一行空的輸出,輸出結(jié)果中出現(xiàn)了一些特例,比如:neo、abca。其中,neo 字符串長(zhǎng)度等于 abc 的長(zhǎng)度,而 abca 的長(zhǎng)度等于 neoj 的長(zhǎng)度。
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) } } }
例子說(shuō)明,string 的賦值并不是原子的。
Go 語(yǔ)言中 string 的內(nèi)存結(jié)果如下,它包含兩部分:Data 表示實(shí)際的數(shù)據(jù)部分,而 Len 表示字符串的長(zhǎng)度。
所以,通過(guò)方法 len 來(lái)計(jì)算字符串的長(zhǎng)度并不會(huì)有性能開銷,len 方法會(huì)直接返回結(jié)構(gòu)體的 Len 屬性;而傳遞字符串類型的參數(shù),使用指針類型和值類型,性能上也不會(huì)有太大差別。
type StringHeader struct { Data uintptr Len int }
字符串的并發(fā)不安全,主要就是給這兩個(gè)字段的賦值,沒(méi)有辦法保證原子性。參考 runtime/string.go 中的源碼,我們可以了解字符串生成過(guò)程。
并發(fā)賦值的情況下,Data 指向的地址和 Len 無(wú)法保證一一對(duì)應(yīng)。所以,通過(guò) Data 獲取到內(nèi)存的首地址,通過(guò) Len 去讀取指定長(zhǎng)度的內(nèi)存時(shí),就會(huì)出現(xiàn)內(nèi)存讀取異常的情況。
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 函數(shù)在字符串拼接的時(shí)候被調(diào)用,我們代碼中創(chuàng)建一個(gè)字符串類型,每次都生成一份新的內(nèi)存空間。特別強(qiáng)調(diào),創(chuàng)建和字符串賦值需要區(qū)分開來(lái)。賦值的過(guò)程其實(shí)是值拷貝,拷貝的便是 StringHeader 結(jié)構(gòu)體。
var name string = "" func main() { blog := name fmt.Println(blog) }
上面的變量 blog 是 name 的值拷貝,底層指向的字符串是同一塊內(nèi)存空間。這個(gè)賦值過(guò)程中,發(fā)生拷貝的只是外層的 StringHeader 對(duì)象。
Go 中通過(guò) unsafe 包可以強(qiáng)制對(duì)內(nèi)存數(shù)據(jù)做類型轉(zhuǎn)換,我們將 blog 和 name 的內(nèi)存地址打印出來(lái)比較一下。最終打印輸出兩個(gè)變量的地址和Data地址??梢钥闯?,賦值前后,Data指向的地址并沒(méi)有發(fā)生變化。
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 并發(fā)不安全讀寫,會(huì)導(dǎo)致線上服務(wù)偶發(fā) panic。比如使用 json 對(duì)內(nèi)存異常的 string 做序列化的時(shí)候。下面的例子中,其中一個(gè)協(xié)程用來(lái)賦值為空,非常容易復(fù)現(xiàn) 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 的地址,而并發(fā)導(dǎo)致 Len 字段有值,最終導(dǎo)致發(fā)生 panic。
競(jìng)態(tài)競(jìng)爭(zhēng)
對(duì)同一個(gè)變量并發(fā)讀寫,如果沒(méi)有使用輔助的同步操作,就會(huì)出現(xiàn)不符合預(yù)期的情況。直白的講,我們開發(fā)完一個(gè)程序之后,針對(duì)同樣的輸入,會(huì)輸出什么結(jié)果,我們是不確定的。
可以參考 The Go Memory Model 的介紹,強(qiáng)調(diào)一下數(shù)據(jù)競(jìng)爭(zhēng)的概念:
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
幸運(yùn)的是,Go 已經(jīng)集成了現(xiàn)成的工具來(lái)診斷數(shù)據(jù)競(jìng)爭(zhēng):-race
。在 go build、或者直接執(zhí)行的時(shí)候,指定 -race
屬性,系統(tǒng)會(huì)做數(shù)據(jù)競(jìng)爭(zhēng)檢測(cè),并打印輸出。
以最近的代碼為例,如果你使用的也是 goland 編譯器,只需要在 Run Configurations / Go tool arguments 中指定 -race
屬性,運(yùn)行程序,就會(huì)出現(xiàn)下面的檢測(cè)結(jié)果:
面對(duì)生產(chǎn)環(huán)境,-race
有比較嚴(yán)重的性能開銷,我們最好是開發(fā)環(huán)境做競(jìng)態(tài)檢測(cè)。
-race
是通過(guò)編譯器注入代碼來(lái)執(zhí)行檢測(cè)的,在函數(shù)執(zhí)行前、執(zhí)行后都會(huì)做內(nèi)存統(tǒng)計(jì)。也就是說(shuō):只有被執(zhí)行到的代碼才能被檢測(cè)到。所以,如果開發(fā)階段做競(jìng)態(tài)檢測(cè)的話,一定要保證代碼被執(zhí)行到了。
再加上埋點(diǎn)的內(nèi)存統(tǒng)計(jì)也是有策略的,也不可能保證存在數(shù)據(jù)競(jìng)爭(zhēng)的代碼就一定會(huì)被檢測(cè)出來(lái),最好可以多執(zhí)行幾次來(lái)避免這種情況。
字符串優(yōu)化
因字符串并發(fā)讀寫導(dǎo)致的 panic,很容易被 Go 的字符串優(yōu)化帶偏。
我在第一次遇到這種情況的時(shí)候,想到的居然是:會(huì)不會(huì)是底層優(yōu)化導(dǎo)致的。因?yàn)榘l(fā)生 panic 的代碼用到了 map 的數(shù)據(jù)結(jié)構(gòu)。這種想法很快被我用測(cè)試用例排除了。
[]byte 到 string 類型轉(zhuǎn)換是比較常規(guī)的操作,正常情況下,轉(zhuǎn)換都會(huì)申請(qǐng)了一份新的內(nèi)存空間。但 Go 為了提高性能,在某些場(chǎng)景下 string 和 []byte 會(huì)共用一份內(nèi)存空間,這種場(chǎng)景下也能寫亂內(nèi)存。
// 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 }
程序中出現(xiàn)問(wèn)題,還是要先充分審查自己開發(fā)的代碼
原文鏈接:https://blog.csdn.net/whynottrythis/article/details/128600337
相關(guān)推薦
- 2022-10-12 Golang?WorkerPool線程池并發(fā)模式示例詳解_Golang
- 2022-05-03 基于R語(yǔ)言?數(shù)據(jù)檢驗(yàn)詳解_R語(yǔ)言
- 2022-03-06 C#多種操作excel的方法比較_C#教程
- 2022-07-15 Android?Flutter繪制扇形圖詳解_Android
- 2023-01-10 詳解C語(yǔ)言中的動(dòng)態(tài)內(nèi)存管理_C 語(yǔ)言
- 2022-03-31 Python機(jī)器學(xué)習(xí)應(yīng)用之基于線性判別模型的分類篇詳解_python
- 2022-05-27 C++左值與右值,右值引用,移動(dòng)語(yǔ)義與完美轉(zhuǎn)發(fā)詳解_C 語(yǔ)言
- 2023-05-08 C++中new和delete匹配使用過(guò)程詳解_C 語(yǔ)言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支