Swift在什麼情況會發生內存訪問沖突詳解

前言

眾所周知,Swift 是一門類型安全的語言,它會通過編譯器報錯來阻止你代碼中不安全的行為。比如變量必須在使用之前聲明、變量被銷毀之後內存不能在訪問、數組越界等問題。

Swift 會通過對於修改同一塊內存,同一時間以互斥訪問權限的方式(同一時間,隻能有一個寫權限),來確保你的代碼不會發生內存訪問沖突。雖然 Swift 是自動管理內存的,在大多數情況下你並不需要關心這個。但理解何種情況下會發生內存訪問沖突也是十分必要的。

首先,來看一下什麼是內存訪問沖突。

內存訪問沖突

當你設值或者讀取變量的值得時候,就會訪問內存。

var age = 10 // 寫權限
print(age) // 讀權限

當我們對同一塊內存,同時進行讀寫操作時,會產生不可預知的錯誤。比如上面的 age,假如在你讀取它值的期間有別的代碼將它設為 20,那麼你讀取到的有可能是 10,也有可能是 20。這就產生瞭問題。

內存訪問沖突:對同一塊內存,同時進行讀寫操作,或者同時進行多個寫入操作時,就會造成內存訪問沖突。

瞭解瞭什麼是內存訪問沖突,下面來看下什麼情況下回造成內存訪問沖突。

In-Out 參數

當 In-Out 參數為全局變量,並且該變量在函數體內被修改時,就會造成內存訪問沖突。比如下面的代碼:

var age = 10

func increment(_ num: inout Int) { // step1
 num += age // step2
}
increment(&age)

increment(:) 在整個函數體內,對所有的 In-Out 參數都有寫權限。在上述代碼中,step1 已經獲得瞭 age 的寫權限,而 step2 有得到瞭 age 的讀權限,這樣就造成瞭同一塊內存,同時進行瞭讀寫操作。從而造成瞭內存訪問沖突。

上面的問題可以通過將 age 拷貝一份來解決:

// step1
var copyOfAge = age
increment(&copyOfAge)
age = copyOfAge

step1 將 age 的值拷貝到另一塊內存上,這樣在函數體內就是存在對 age 的讀權限和對 copyOfAge 的寫權限,因為 age 和 copyOfAge 是兩塊內存,所以就不會造成內存訪問沖突。

結構體的 mutating 函數

對於結構體的 mutating 函數來說,它整個函數體都有 self 的寫權限。

struct Person {
 var age: Int
 mutating func increment(_ num: inout Int) { 
  age += num 
 }
}

var p1 = Person(age: 10)
p1.increment(&p1.age)

上述的代碼編譯器會報錯:Overlapping accesses to 'p1', but modification requires exclusive access; consider copying to a local variable。很明顯這是一個內存訪問沖突。

In-Out 參數獲得瞭 p1 的寫權限;mutating 函數也獲得瞭 p1 的寫權限。同一塊內存,同時有兩個寫操作。造成內存訪問沖突。可以通過同上的拷貝操作來解決。

值類型的屬性

對於結構體、枚舉、元祖等值類型來說,修改它們的屬性就相當於修改它們整個的值。比如下面的代碼:

func increment(_ num1: inout Int, _ num2: inout Int) {
 print(num1 + num2)
}

var tuple = (age: 10, height: 20)
increment(&tuple.age, &tuple.height)

&tuple.age 拿到瞭 tuple 的寫權限,&tuple.height 又拿瞭 tuple 的寫權限。同一塊內存,同時有兩個寫操作。造成內存訪問沖突。

這個問題可以通過局部變量來解決:

func someFunction() {
 var tuple = (age: 10, height: 20)
 increment(&tuple.age, &tuple.height)
}

因為在 someFunction() 函數裡,age 和 height 沒有產生任何的交互(沒有在其期間去讀取或者寫入 age 和 height),所以編譯器可以保證內存安全。

PS:關於評論區的問題,在 someFunction() 函數裡沒有任何交互是什麼意思?

答:在someFunction() 裡,編譯器可以保證沒有別的線程來讀取或者修改 tuple。因此,可以保證內存安全。而對於全局變量,編譯器無法保證是否有別的線程在讀取或者修改。

下面的代碼就是在函數體內有交互的代碼,雖然是局部變量,但涉及多個線程修改 tuple 的值,因此會造成內存訪問沖突:

func someFunction() {
 var tuple = (age: 10, height: 20)
 
 DispatchQueue.main.async {
  tuple.age += 10
 }
 
 DispatchQueue.main.async {
  increment(&tuple.age, &tuple.height)
 }
}

總結

對同一塊內存,同時進行讀寫操作,或者同時進行多個寫入操作時,就會造成內存訪問沖突。

會造成內存訪問沖突的情況:

  • In-Out 為全局參數,並且在函數體內修改瞭它。
  • 結構體的 mutating 函數內修改結構體的值。
  • 同一值類型的多個屬性當做函數的 In-Out 參數。

到此這篇關於Swift在什麼情況會發生內存訪問沖突的文章就介紹到這瞭,更多相關Swift內存訪問沖突內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!