為什么說atomic是不安全的?

atomic是否是安全的?

首先要明確的是, 實際上atomic是安全的, 而且是絕對安全的

atomic實際上就是原子操作, 所謂原子, 就是不可再分割, 已經是最小的操作單位了(所謂操作指的是對內存的讀寫), 很多在討論OC下的atomic不安全, 不能保證數據的并發性, 認為atomic本身是不安全的, 實際上, 并非atomic不安全.

一個數據的線程安全, 簡單說就是這部分的數據即使有多個線程同時讀寫, 也不會出現數據錯亂的情況, 內存的最后狀態總是可以預見的, 如果這塊內存的數據被一個多線程讀寫之后, 出現的結果是不可預見的, 那么就可以說這塊內存是"線程不安全的".

  • atomic: 原子屬性, 線程安全, 效率較低. 可以當成人通過一扇門, 一次只能通過一個人.
  • nonatomic: 非原子屬性, 線程不安全, 效率高. 可以多個人同時進出門.

atomic實際上相當于一個引用計數器, 如果被標記了atomic, 那么被標記了的內存本身就有了一個引用計數器, 第一個占用這塊內存的線程, 會給這個計數器+1, 在這個線程操作這塊內存期間, 其他線程在訪問這個內存的時候, 如果發現"引用計數器"不為0, 則阻塞, 實際上阻塞并不等于休眠, 它是基于CPU輪詢, 休眠除非被喚醒, 否則無法繼續執行, 阻塞則不同, 每個CPU輪詢片到這個線程的時候都會嘗試繼續往下執行, 可見阻塞相對于休眠來講, 阻塞是主動的, 休眠是被動的, 如果引用計數器為0, 輪詢片到這,則先給這塊內存的引用計數器+1, 然后再去操作, atomic實現操作序列化的具體過程大概是就是這樣.

如果一個數據占的內存特別大, 那么讀寫這塊內存數據需要的時間也就越長, 如果這個時間長度遠遠超過線程調度的輪詢片, 那么就有極大可能出現并發問題.(一塊內存在被一條線程讀寫的時間, 快到其他線程來到的時候就已經結束, 那么就不會出現線程安全問題, 反之, 就會出現線程安全的問題)

以OC下的NSArray *為例子, 如果一個多線程操作這個數據, 會有兩個層級的并發問題:

  1. 指針本身
  2. 指針所指向的內存

指針本身也是占用內存的, 并且一定是8個字節, 第二部分, 指針所指向的內存, 這個占多少字節就不一定了, 有可能非常大, 有可能很小.

所以在考慮NSArray *array 這個數據array多線程操作的時候, 必須分成兩部分來描述, 一個是&array這個指針本身, 另一個則是它所指向的內存的array. &array是一塊8字節的內存空間, 里面的值就是有n字節內存array的首地址.

現在聯系上atomic, 如果是@property(atomic)NSArray *array, 會有什么影響? 首先要明確的是atomic作為修飾符修飾的是這個指針即&array, 跟array沒有任何關系. 被atomic修飾之后, 你不可能隨意去多線程操作&array, 這個8字節的內存, 但是對8字節里面所指向的n字節沒有任何限制, 這就是會出現atomic線程不安全的真相.

本身atomic修飾的是一個指針, 讓這個指針所指的內存是線程安全的, atomic沒有任何問題, 有問題的是未對n字節做任何的限制.
我們知道, 這個8字節里面存儲的數據, 是n字節數據的頭地址, 如果更改8字節數據的內容, 那么最后通過這個指針訪問到的數據就會完全不一樣. 8字節相當于樓管, 里面的數據相當于整棟樓的鑰匙, 給你不同的鑰匙, 你是不是就進的是不同的房間? 通過atomic我們可以保證這個"指針"被有序的訪問, 也僅僅只能保證到這.

首先構建一個不加任何保護的場景, 看看會出現什么問題

現在我們有一個8字節的指針, 假如我們做一個初始化NSArray *array = [[NSArray alloc] init] 這個操作, 實際上這個操作有兩個意思:

  1. 給8字節的&array賦值
  2. 開辟了一塊n字節的內存去給array

我們只說這8字節的地址賦值, 如果沒有atomic修飾, 并且假設現在有兩個線程正在操作這個指針, 一個就是上面的初始化線程, 另一個線程就是讀這個8字節的指針. 首先, 假如8字節內部存放的是0x1234567890這個內存地址, 8字節需要寫入這個值, 但與此同時, 另一個讀線程現在要讀這個8字節里面的值. 假如這個8字節只寫了一半的時候另一個線程來讀, 那它讀到的可能是0x1234560000, 實際上, 等它讀完, 寫線程仍然還未完成, 這個時候, [[NSArray alloc] init] 的頭部地址正確的應該是0x1234567890, 而讀線程讀到的是0x1234560000 這時候會出現什么情況? 最好的情況, 無非就是個野指針, 因為誰也不知道這塊地址是否有效或者是否有什么重要的數據. 最壞的情況, 這個野指針指向的是重要的一段數據, 后果可想而知.

所以atomic的意義就在于此, 在0x1234567890寫完之前, 讀線程無法讀取, 同樣的道理, 在讀線程正在讀的過程中, 寫線程是無法改變8字節的.

atomic能避免這8字節的值因為多線程的原因被意外破獲, 僅此而已

考慮場景二, 假如現在有atomic修飾, 假如現在有兩個線程正在操作這個指針, 根據上面的結論, 這兩個線程"先后"正確的獲取到了內存地址, 也就是說, 都先后, 正確的找到了8字節內容所指向的n字節內容, 雖然找到這n個字節內容的順序有先后, 但是不影響這兩個線程同時去操作這n個字節的數據.

但是這樣問題又來了, 兩個線程同時去操作n字節內容, 如果兩個線程都是讀線程, 一般不會有問題, 但是假如至少有一個是寫線程, 那問題又來了, 還是一個讀寫同步的問題, 因此atomic雖然規范了找到這n字節內容的先后順序, 但是不能規范對這n字節內容的讀寫, 這就是atomic的局限性.

至于為什么基本都是使用nonatomic來修飾OC中的屬性, 這是因為這些操作都是在主線程中做的操作, 沒有多線程的情況, 也就不會有并發的問題了.

Reference

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容