Go 1-19 中的原子指針

在計算機編程中,” 原子” 是指一次執行一個操作。Objective-C有原子屬性,它確保了從不同的線程對一個屬性進行安全的讀寫。在Objective-C中,它是與不可變類型一起使用的。這是因爲爲了改變不可變類型,實際上是 “重新創建” 它。換句話說,在你的代碼中改變一個不可變的類型不會導致編譯器拋出一個錯誤。然而,當你這樣做的時候,它會實例化一個新的對象。一個典型的例子是Goappend函數,它每次調用都會產生一個新的切片。在Objective-C中,原子屬性將確保操作是一個接一個進行的,以防止線程同時訪問一個內存地址。由於Go是多線程的,它也支持原子操作。Go 1.19 引入了新的原子類型。我最喜歡的新增類型是atomic.Pointer,它爲atomic.Value提供了一個平滑的替代方案。它也很好地展示了泛型是如何增強開發者體驗的。

atomic.Pointer 原子指針

atomic.Pointer 是一個通用類型。與Value不同,它不需要斷言你的存儲值就可以訪問。下面是一段定義和存儲指針的代碼:

package main
import (
 "fmt"
 "net"
 "sync/atomic"
)
type ServerConn struct {
 Connection net.Conn
 ID string
 Open bool
}
func main() {
 p := atomic.Pointer[ServerConn]{}
 s := ServerConn{ ID : "first_conn"}
 p.Store( &)
 fmt.Println(p.Load()) // Will display value stored.
}

將變量p實例化爲一個指針結構字面量,然後將變量s的指針存儲在p中,s代表一個服務器連接。至此,我們已經通過了實現原子性的第一步。通過將變量存儲爲原子值,我們將確保沒有同時訪問內存地址的情況。例如,如果同時並行讀取和寫入map,將導致程序恐慌。鎖是防止這些恐慌發生的一個好方法,原子操作也是如此。

關於原子指針的使用示例

在之前提供的代碼基礎上,我將使用一個atomic.Pointer來每 13 秒重新創建一個數據庫連接。首先編寫一個函數,用來記錄每 10 秒的連接 ID。這將是查看新連接對象是否被傳播的機制。然後,將定義一個內聯函數,每 13 秒改變一次連接。下面是代碼的樣子:

...
func ShowConnection(p * atomic.Pointer[ServerConn]){
for {
  time.Sleep(10 * time.Second)
  fmt.Println(p, p.Load())
 }
 
}
func main() {
 c := make(chan bool)
 p := atomic.Pointer[ServerConn]{}
 s := ServerConn{ ID : "first_conn"}
 p.Store( &)
 go ShowConnection(&p)
 go func(){
   for {
    time.Sleep(13 * time.Second)
    newConn := ServerConn{ ID : "new_conn"}
    p.Swap(&newConn)
   }
 }()
 <- c
}

ShowConnection是作爲一個Goroutine調用,內聯函數將實例化一個新的ServerConn對象,並將其與當前連接對象交換。這在指針上是可行的,但是,這需要實現一個 “鎖定 - 解鎖 “系統。atomic包對此進行了抽象,並確保每個加載和保存都是一個接一個地處理。這是一個簡單的例子,也是一個不那麼常見的用例。另外,使用atomic.Pointer可能是一個 “過度工程” 的案例,因爲我程序的Goroutines是在不同時間段運行的。我將使用 Go 的race標誌來查看我的程序的Goroutines是否在同一時間訪問同一個內存地址。下面,將使用指針方式重寫上述代碼,而不是atomic.Pointer方式。

數據競爭

“當兩個Goroutine同時訪問同一個變量,並且至少有一個訪問是寫的時候,就會發生數據競爭”。爲了快速驗證數據競賽,你可以執行go run,加上標誌race參數來進行測試。爲了演示原子類型如何防止這種情況,我們來重寫上面的例子,使用經典的 Go 指針。下面是代碼的樣子:

package main

import (
 "fmt"
 "net"
 "time"
)
type ServerConn struct {
 Connection net.Conn
 ID string
 Open bool
}
func ShowConnection(p * ServerConn){
for {
  time.Sleep(10 * time.Second)
  fmt.Println(p, *p)
 }
 
}
func main() {
 c := make(chan bool)
 p :=  ServerConn{ ID : "first_conn"}
 go ShowConnection(&p)
 go func(){
   for {
    time.Sleep(13 * time.Second)
    newConn := ServerConn{ ID : "new_conn"}
    p = newConn
   }
 }()
 <- c
}

在檢查了數據競爭後,終端上的輸出是這樣的:

~/go/src/atomic$ go run -race main_classic.go 
&{<nil> first_conn false} {<nil> first_conn false}
==================
WARNING: DATA RACE
Write at 0x00c000074570 by `Goroutine` 8:
  main.main.func1()
      /home/cheikh/go/src/atomic/main_classic.go:37 +0x6fPrevious read at 0x00c000074570 by `Goroutine` 7:
  runtime.convT()
      /usr/lib/go-1.18/src/runtime/iface.go:321 +0x0
  main.ShowConnection()
      /home/cheikh/go/src/atomic/main_classic.go:19 +0x65
  main.main.func2()
      /home/cheikh/go/src/atomic/main_classic.go:30 +0x39`Goroutine` 8 (running) created at:
  main.main()
      /home/cheikh/go/src/atomic/main_classic.go:33 +0x16e`Goroutine` 7 (running) created at:
  main.main()
      /home/cheikh/go/src/atomic/main_classic.go:30 +0x104
==================
&{<nil> new_conn false} {<nil> new_conn false}
&{<nil> new_conn false} {<nil> new_conn false}
&{<nil> new_conn false} {<nil> new_conn false}

雖然這兩個函數在不同的時間間隔運行,但它們在某些時候會發生碰撞(譯者注:處理耗時變化,會導致可能退化爲並行讀寫)。有原子指針的代碼沒有返回關於數據競爭的反饋。這是一個例子,說明原子指針在多線程環境中表現得更好。

總結

Go原子類型是管理共享資源的一種簡單方法。它消除了不斷實現互斥來控制資源訪問的需要。這並不意味着mutex已經過時了,因爲在某些操作中仍然需要它們。總之,atomic.Pointer是將原子內存原語引入你的程序的一個好方法。它是一個簡單防止數據競爭的方法,而不需要花哨的互斥代碼。訪問這個鏈接,可以看到這篇文章中使用的代碼。

鏈接:https://go.dev/doc/articles/race_detector

原文地址:

https://medium.com/@cheikhhseck/atomic-pointers-in-go-1-19-cad312f82d5b

原文作者:

Cheikh seck

本文永久鏈接:

https://github.com/gocn/translator/blob/master/2022/2022/w32_atomic_pointers_in_go_1_19.md

譯者:Fivezh

校對:zhuyaguang

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/EGMKbpPxrtO1HToYVOYwzw