亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 系統 > iOS > 正文

探究iOS多線程究竟不安全在哪里?

2020-07-26 02:56:10
字體:
來源:轉載
供稿:網友

前言

共享狀態,多線程共同訪問某個對象的property,在iOS編程里是很普遍的使用場景,我們就從Property的多線程安全說起。

Property

當我們討論property多線程安全的時候,很多人都知道給property加上atomic attribute之后,可以一定程度的保障多線程安全,類似:

@property (atomic, strong) NSString*   userName;

事情并沒有看上去這么簡單,要分析property在多線程場景下的表現,需要先對property的類型做區分。

我們可以簡單的將property分為值類型和對象類型,值類型是指primitive type,包括int, long, bool等非對象類型,另一種是對象類型,聲明為指針,可以指向某個符合類型定義的內存區域。

上述代碼中userName明顯是個對象類型,當我們訪問userName的時候,訪問的有可能是userName本身,也有可能是userName所指向的內存區域。

比如:

self.userName = @"peak";

是在對指針本身進行賦值。而

[self.userName rangeOfString:@"peak"];

是在訪問指針指向的字符串所在的內存區域,這二者并不一樣。

所以我們可以大致上將property分為三類:

分完類之后,我們需要明白這三類property的內存模型。

Memory Layout

當我們討論多線程安全的時候,其實是在討論多個線程同時訪問一個內存區域的安全問題。針對同一塊區域,我們有兩種操作,讀(load)和寫(store),讀和寫同時發生在同一塊區域的時候,就有可能出現多線程不安全。所以展開討論之前,先要明白上述三種property的內存模型,可用如下圖示:

以64位系統為例,指針NSString*是8個字節的內存區域,int count是個4字節的區域,而@“Peak”是一塊根據字符串長度而定的內存區域。

當我們訪問property的時候,實際上是訪問上圖中三塊內存區域。

self.userName = @"peak";

是修改第一塊區域。

self.count = 10;

是在修改第二塊區域。

[self.userName rangeOfString:@"peak"];

是在讀取第三塊區域。

不安全的定義

明白了property的類型以及他們對應的內存模型,我們再來看看不安全的定義。Wikipedia如是說:

 A piece of code is thread-safe if it manipulates shared data structures only in a manner that guarantees safe execution by multiple threads at the same time
這段定義看起來還是有點抽象,我們可以將多線程不安全解釋為:多線程訪問時出現意料之外的結果。這個意料之外的結果包含幾種場景,不一定是指crash,后面再一一分析。

先來看下多線程是如何同時訪問內存的。不考慮CPU cache對變量的緩存,內存訪問可以用下圖表示:

從上圖中可以看出,我們只有一個地址總線,一個內存。即使是在多線程的環境下,也不可能存在兩個線程同時訪問同一塊內存區域的場景,內存的訪問一定是通過一個地址總線串行排隊訪問的,所以在繼續后續之前,我們先要明確幾個結論:

結論一:內存的訪問時串行的,并不會導致內存數據的錯亂或者應用的crash。

結論二:如果讀寫(load or store)的內存長度小于等于地址總線的長度,那么讀寫的操作是原子的,一次完成。比如bool,int,long在64位系統下的單次讀寫都是原子操作。

接下來我們根據上面三種property的分類逐一看下多線程的不安全場景。

值類型Property

先以BOOL值類型為例,當我們有兩個線程訪問如下property的時候:

@property (nonatomic, assgin) BOOL isDeleted;//thread 1bool isDeleted = self.isDeleted;//thread 2self.isDeleted = false;

線程1和線程2,一個讀(load),一個寫(store),對于BOOL isDeleted的訪問可能有先后之分,但一定是串行排隊的。而且由于BOOL大小只有1個字節,64位系統的地址總線對于讀寫指令可以支持8個字節的長度,所以對于BOOL的讀和寫操作我們可以認為是原子的,所以當我們聲明BOOL類型的property的時候,從原子性的角度看,使用atomic和nonatomic并沒有實際上的區別(當然如果重載了getter方法就另當別論了)。

如果是int類型呢?

@property (nonatomic, assgin) int count;//thread 1int curCount = self.count;//thread 2self.count = 1;

同理int類型長度為4字節,讀和寫都可以通過一個指令完成,所以理論上讀和寫操作都是原子的。從訪問內存的角度看nonatomic和atomic也并沒有什么區別。

atomic到底有什么用呢?據我所知,用處有二:

用處一: 生成原子操作的getter和setter。

設置atomic之后,默認生成的getter和setter方法執行是原子的。也就是說,當我們在線程1執行getter方法的時候(創建調用棧,返回地址,出棧),線程B如果想執行setter方法,必須先等getter方法完成才能執行。舉個例子,在32位系統里,如果通過getter返回64位的double,地址總線寬度為32位,從內存當中讀取double的時候無法通過原子操作完成,如果不通過atomic加鎖,有可能會在讀取的中途在其他線程發生setter操作,從而出現異常值。如果出現這種異常值,就發生了多線程不安全。

用處二:設置Memory Barrier

對于Objective C的實現來說,幾乎所有的加鎖操作最后都會設置memory barrier,atomic本質上是對getter,setter加了鎖,所以也會設置memory barrier。官方文檔表述如下:

Note: Most types of locks also incorporate a memory barrier to ensure that any preceding load and store instructions are completed before entering the critical section.

memory barrier有什么用處呢?

memory barrier能夠保證內存操作的順序,按照我們代碼的書寫順序來。聽起來有點不可思議,事實是編譯器會對我們的代碼做優化,在它認為合理的場景改變我們代碼最終翻譯成的機器指令順序。也就是說如下代碼:

self.intA = 0; //line 1self.intB = 1; //line 2

編譯器可能在一些場景下先執行line2,再執行line1,因為它認為A和B之間并不存在依賴關系,雖然在代碼執行的時候,在另一個線程intA和intB存在某種依賴,必須要求line1先于line2執行。

如果設置property為atomic,也就是設置了memory barrier之后,就能夠保證line1的執行一定是先于line2的,當然這種場景非常罕見,一則是出現變量跨線程訪問依賴,二是遇上編譯器的優化,兩個條件缺一不可。這種極端的場景下,atomic確實可以讓我們的代碼更加多線程安全一點,但我寫iOS代碼至今,還未遇到過這種場景,較大的可能性是編譯器已經足夠聰明,在我們需要的地方設置memory barrier了。

是不是使用了atomic就一定多線程安全呢?我們可以看看如下代碼:

@property (atomic, assign) int intA;//thread Afor (int i = 0; i < 10000; i ++) { self.intA = self.intA + 1; NSLog(@"Thread A: %d/n", self.intA);}//thread Bfor (int i = 0; i < 10000; i ++) { self.intA = self.intA + 1; NSLog(@"Thread B: %d/n", self.intA);}

即使我將intA聲明為atomic,最后的結果也不一定會是20000。原因就是因為self.intA = self.intA + 1;不是原子操作,雖然intA的getter和setter是原子操作,但當我們使用intA的時候,整個語句并不是原子的,這行賦值的代碼至少包含讀取(load),+1(add),賦值(store)三步操作,當前線程store的時候可能其他線程已經執行了若干次store了,導致最后的值小于預期值。這種場景我們也可以稱之為多線程不安全。

指針Property

指針Property一般指向一個對象,比如:

@property (atomic, strong) NSString*   userName;

無論iOS系統是32位系統還是64位,一個指針的值都能通過一個指令完成load或者store。但和primitive type不同的是,對象類型還有內存管理的相關操作。在MRC時代,系統默認生成的setter類似如下:

- (void)setUserName:(NSString *)userName { if(_uesrName != userName) { [userName retain]; [_userName release]; _userName = userName; }}

不僅僅是賦值操作,還會有retain,release調用。如果property為nonatomic,上述的setter方法就不是原子操作,我們可以假設一種場景,線程1先通過getter獲取當前_userName,之后線程2通過setter調用[_userName release];,線程1所持有的_userName就變成無效的地址空間了,如果再給這個地址空間發消息就會導致crash,出現多線程不安全的場景。

到了ARC時代,Xcode已經替我們處理了retain和release,絕大部分時候我們都不需要去關心內存的管理,但retain,release其實還是存在于最后運行的代碼當中,atomic和nonatomic對于對象類的property聲明理論上還是存在差異,不過我在實際使用當中,將NSString*設置為nonatomic也從未遇到過上述多線程不安全的場景,極有可能ARC在內存管理上的優化已經將上述場景處理過了,所以我個人覺得,如果只是對對象類property做read,write,atomic和nonatomic在多線程安全上并沒有實際差別。

指針Property指向的內存區域

這一類多線程的訪問場景是我們很容易出錯的地方,即使我們聲明property為atomic,依然會出錯。因為我們訪問的不是property的指針區域,而是property所指向的內存區域??梢钥慈缦麓a:

@property (atomic, strong) NSString*   stringA;//thread Afor (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.stringA = @"a very long string"; } else { self.stringA = @"string"; } NSLog(@"Thread A: %@/n", self.stringA);}//thread Bfor (int i = 0; i < 100000; i ++) { if (self.stringA.length >= 10) { NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)]; } NSLog(@"Thread B: %@/n", self.stringA);}

雖然stringA是atomic的property,而且在取substring的時候做了length判斷,線程B還是很容易crash,因為在前一刻讀length的時候self.stringA = @"a very long string";,下一刻取substring的時候線程A已經將self.stringA = @"string";,立即出現out of bounds的Exception,crash,多線程不安全。

同樣的場景還存在對集合類操作的時候,比如:

@property (atomic, strong) NSArray*   arr;//thread Afor (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.arr = @[@"1", @"2", @"3"]; } else { self.arr = @[@"1"]; } NSLog(@"Thread A: %@/n", self.arr);}//thread Bfor (int i = 0; i < 100000; i ++) { if (self.arr.count >= 2) { NSString* str = [self.arr objectAtIndex:1]; } NSLog(@"Thread B: %@/n", self.arr);}

同理,即使我們在訪問objectAtIndex之前做了count的判斷,線程B依舊很容易crash,原因也是由于前后兩行代碼之間arr所指向的內存區域被其他線程修改了。

所以你看,真正需要操心的是這一類內存區域的訪問,即使聲明為atomic也沒有用,我們平常App出現莫名其妙難以重現的多線程crash多是屬于這一類,一旦在多線程的場景下訪問這類內存區域的時候,要提起十二分的小心。如何避免這類crash后面會談到。

Property多線程安全小結:

簡而言之,atomic的作用只是給getter和setter加了個鎖,atomic只能保證代碼進入getter或者setter函數內部時是安全的,一旦出了getter和setter,多線程安全只能靠程序員自己保障了。所以atomic屬性和使用property的多線程安全并沒什么直接的聯系。另外,atomic由于加鎖也會帶來一些性能損耗,所以我們在編寫iOS代碼的時候,一般聲明property為nonatomic,在需要做多線程安全的場景,自己去額外加鎖做同步。

如何做到多線程安全?

討論到這里,其實怎么做到多線程安全也比較明朗了,關鍵字是atomicity(原子性),只要做到原子性,小到一個primitive type變量的訪問,大到一長段代碼邏輯的執行,原子性能保證代碼串行的執行,能保證代碼執行到一半的時候,不會有另一個線程介入。

原子性是個相對的概念,它所針對的對象,粒度可大可小。

比如下段代碼:

if (self.stringA.length >= 10) { NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];}

是非原子性的。

但加鎖以后:

//thread A[_lock lock];for (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.stringA = @"a very long string"; } else { self.stringA = @"string"; } NSLog(@"Thread A: %@/n", self.stringA);}[_lock unlock];//thread B[_lock lock];if (self.stringA.length >= 10) { NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];}[_lock unlock];

整段代碼就具有原子性了,就可以認為是多線程安全了。

再比如:

if (self.arr.count >= 2) { NSString* str = [self.arr objectAtIndex:1];}

是非原子性的。

//thread A[_lock lock];for (int i = 0; i < 100000; i ++) { if (i % 2 == 0) { self.arr = @[@"1", @"2", @"3"]; } else { self.arr = @[@"1"]; } NSLog(@"Thread A: %@/n", self.arr);}[_lock unlock]; //thread B[_lock lock];if (self.arr.count >= 2) { NSString* str = [self.arr objectAtIndex:1];}[_lock unlock];

是具有原子性的。注意,讀和寫都需要加鎖。

這也是為什么我們在做多線程安全的時候,并不是通過給property加atomic關鍵字來保障安全,而是將property聲明為nonatomic(nonatomic沒有getter,setter的鎖開銷),然后自己加鎖。

如何使用哪種鎖?

iOS給代碼加鎖的方式有很多種,常用的有:

  1. @synchronized(token)
  2. NSLock
  3. dispatch_semaphore_t
  4. OSSpinLock

這幾種鎖都可以帶來原子性,性能的損耗從上至下依次更小。

我個人建議是,在編寫應用層代碼的時候,除了OSSpinLock之外,哪個順手用哪個。相較于這幾個鎖的性能差異,代碼邏輯的正確性更為重要。而且這幾者之間的性能差異對用戶來說,絕大部分時候都感知不到。

當然我們也會遇到少數場景需要追求代碼的性能,比如編寫framework,或者在多線程讀寫共享數據頻繁的場景,我們需要大致了解鎖帶來的損耗到底有多少。

官方文檔有個數據,使用Intel-based iMac with a 2 GHz Core Duo processor and 1 GB of RAM running OS X v10.5測試,獲取mutex有大概0.2ms的損耗,我們可以認為鎖帶來的損耗大致在ms級別。

Atomic Operations

其實除了各種鎖之外,iOS上還有另一種辦法來獲取原子性,使用Atomic Operations,相比鎖的損耗要小一個數量級左右,在一些追求高性能的第三方Framework代碼里可以看到這些Atomic Operations的使用。這些atomic operation可以在/usr/include/libkern/OSAtomic.h中查到:

比如

_intA ++;

是非原子性的。

OSAtomicIncrement32(&(_intA));

是原子性的,多線程安全的。

Atomic Operation只能應用于32位或者64位的數據類型,在多線程使用NSString或者NSArray這類對象的場景,還是得使用鎖。

大部分的Atomic Operation都有OSAtomicXXX,OSAtomicXXXBarrier兩個版本,Barrier就是前面提到的memory barrier,在多線程多個變量之間存在依賴的時候使用Barrier的版本,能夠保證正確的依賴順序。

對于平時編寫應用層多線程安全代碼,我還是建議大家多使用@synchronized,NSLock,或者dispatch_semaphore_t,多線程安全比多線程性能更重要,應該在前者得到充分保證,猶有余力的時候再去追求后者。

盡量避免多線程的設計

無論我們寫過多少代碼,都必須要承認多線程安全是個復雜的問題,作為程序員我們應該盡可能的避免多線程的設計,而不是去追求高明的使用鎖的技能。

后面我會寫一篇文章,介紹函數式編程及其核心思想,即使我們使用非函數式的編程語言,比如Objective C,也能極大的幫助我們避免多線程安全的問題。

總結

iOS下多線程不安全的分析至此結束了,如何編寫多線程安全的代碼,說到底還是在于對memory layout和原子性的理解,也希望這篇文章將atomic和nonatomic的真正區別解釋清楚了:)。如果有疑問大家可以留言交流。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
45www国产精品网站| 欧美夫妻性生活视频| 亚洲摸下面视频| 久久99精品久久久久久青青91| 136fldh精品导航福利| 亚洲直播在线一区| 91影视免费在线观看| 国自产精品手机在线观看视频| 2023亚洲男人天堂| 欧美午夜精品久久久久久人妖| 久久精品免费电影| 亚洲男女性事视频| 日韩精品视频在线| 亚洲精品久久久一区二区三区| 日韩免费av片在线观看| 久久99国产精品久久久久久久久| 97在线视频精品| 精品国产自在精品国产浪潮| 国产成人精品电影久久久| 久久69精品久久久久久国产越南| 亚洲高清不卡av| 国产精品久久久久久久久久久新郎| 精品日韩视频在线观看| 日韩欧美国产高清91| 国产日韩精品电影| 久久深夜福利免费观看| 91沈先生在线观看| 国产拍精品一二三| 日韩视频免费大全中文字幕| 中日韩美女免费视频网址在线观看| 日韩美女视频中文字幕| 国产精品狼人色视频一区| 欧美大片在线免费观看| 日韩精品免费在线观看| 久久久亚洲国产天美传媒修理工| 欧美国产日韩二区| 欧美精品成人91久久久久久久| 欧美日韩在线视频一区| 欧美一级bbbbb性bbbb喷潮片| 久久视频中文字幕| 国产精品视频精品视频| 国产精品99久久久久久www| 成人av资源在线播放| 欧美高清视频免费观看| 91久久精品国产| 91成人国产在线观看| 亚洲欧洲美洲在线综合| 精品国产自在精品国产浪潮| 欧美夫妻性生活视频| 欧美成人全部免费| 国内精品久久久久伊人av| 日韩精品免费观看| 久久久国产精品免费| 亚洲福利视频网| 欧美俄罗斯性视频| 午夜精品一区二区三区在线播放| 国产视频丨精品|在线观看| 久久成人精品视频| 91极品视频在线| 91精品国产91久久久久久久久| 91热福利电影| 久久久久五月天| 欧美大片网站在线观看| 亚洲18私人小影院| 国产精品久久久久久久久免费| 亚洲自拍偷拍网址| 国内外成人免费激情在线视频网站| 成人在线国产精品| 国产成人一区二区三区电影| 久久久久日韩精品久久久男男| 久久免费在线观看| 成人福利网站在线观看| 国产精品十八以下禁看| 精品久久久久久久久国产字幕| 中文字幕在线看视频国产欧美在线看完整| 欧美日本在线视频中文字字幕| 欧美一级免费看| 成人一区二区电影| 久久久国产一区| 一本一本久久a久久精品牛牛影视| 亚洲国产欧美自拍| 中文字幕亚洲二区| 亚洲石原莉奈一区二区在线观看| 7777精品视频| 韩国美女主播一区| 久久精品在线视频| 一本大道香蕉久在线播放29| 日韩电影中文字幕在线| 精品国产乱码久久久久久天美| 国产精品黄视频| 一区二区三区动漫| 亚洲一级一级97网| 在线日韩日本国产亚洲| 最近2019中文字幕mv免费看| 78m国产成人精品视频| 精品视频久久久久久| 久久久久久18| 久久精品久久久久电影| 亚洲一区二区国产| 国产a∨精品一区二区三区不卡| 国产精品成人久久久久| 亚洲已满18点击进入在线看片| 国产精品成人一区二区三区吃奶| 在线观看视频99| 久久理论片午夜琪琪电影网| 不卡av日日日| 91精品国产高清久久久久久91| 九九久久久久99精品| 国产精品美女久久久免费| 久久成人这里只有精品| 久久天天躁狠狠躁夜夜躁2014| 国产丝袜高跟一区| 欧美激情影音先锋| 国产有码在线一区二区视频| 91精品国产精品| 亚洲自拍偷拍色片视频| 国外成人在线播放| 日av在线播放中文不卡| 国产精品久久网| 亚洲欧美在线免费观看| 国产精品日韩电影| 日韩精品免费综合视频在线播放| 精品久久久久国产| 亚洲丝袜在线视频| 国产一区二区黄| 久久久亚洲网站| 亚洲黄色av网站| 国内偷自视频区视频综合| 国产精品亚洲网站| 91视频国产高清| 日韩在线不卡视频| 亚洲美女久久久| 尤物九九久久国产精品的分类| 亚洲人高潮女人毛茸茸| 国产日韩欧美日韩| 国产视频精品xxxx| 色诱女教师一区二区三区| 亚洲第一在线视频| 久久国产精品久久国产精品| 日韩av一区在线| 久久99精品久久久久久琪琪| 亚洲午夜久久久久久久| 一区二区三区亚洲| 亚洲欧美日韩久久久久久| xxx欧美精品| 精品久久久久久久久久久久| 国产精品偷伦一区二区| 亚洲另类激情图| www.日韩不卡电影av| 中文字幕欧美精品日韩中文字幕| 欧美性高潮在线| 久久久久久国产精品美女| 国产在线拍揄自揄视频不卡99| 91精品国产色综合久久不卡98口| 国产精品女主播| 亚洲精品自在久久| 欧美电影《睫毛膏》| 欧美亚洲在线播放| 亚洲免费av网址| 欧美日韩免费在线| 精品成人69xx.xyz| 国产日韩欧美在线观看| 欧美日韩国产成人在线观看| 九九热99久久久国产盗摄|