Swift 什麼狀況會發生內存訪問衝突

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

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

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

內存訪問衝突

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

var age = 10 // 寫權限
print(age) // 讀權限
複製代碼

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

內存訪問衝突:對同一塊內存,同時進行讀寫操做,或者同時進行多個寫入操做時,就會形成內存訪問衝突。ide

瞭解了什麼是內存訪問衝突,下面來看下什麼狀況下回形成內存訪問衝突。函數

In-Out 參數

當 In-Out 參數爲全局變量,而且該變量在函數體內被修改時,就會形成內存訪問衝突。好比下面的代碼:ui

var age = 10

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

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

上面的問題能夠經過將 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 參數。
相關文章
相關標籤/搜索