atomic

atomic 在 setter 方法里加了鎖,防止了多線程一直去寫這個 property,造成難以預(yù)計的數(shù)值弟头。

當(dāng)屬性使用 atomic 修飾時,它的讀和寫是原子性的:當(dāng)線程 A 進(jìn)行寫操作群井,這時其他線程的讀或者寫操作會因為該操作而等待。當(dāng) A 線程的寫操作結(jié)束后毫胜,B 線程進(jìn)行寫操作书斜,然后當(dāng) A 線程需要讀操作時,獲得了在 B 線程中修改的值酵使。如果有 C 線程在 A 線程讀操作之前 release 了該屬性荐吉,可能導(dǎo)致程序崩潰。

導(dǎo)致崩潰并不是線程安全問題口渔。所謂線程安全是保證同一時間只有一個線程對該內(nèi)存進(jìn)行訪問样屠。只要我們使用 getter、setter 方法來訪問,上面的表述中的每一個步驟都只有一條線程在訪問該內(nèi)存痪欲,哪個線程會獲得鎖完全取決于代碼順序混巧,這個崩潰就是程序員自身的問題了。如果繞開 getter勤揩、setter 方法訪問這個屬性,才會造成線程不安全秘蛔,比如使用 KVC陨亡。

一、atomic 是絕對安全的

在 64 位的操作系統(tǒng)下深员,所有類型的指針(包括 void *)都是占用 8 個字節(jié)的负蠕。超過 4 個字節(jié)的基本類型數(shù)據(jù)都會有線程并發(fā)的問題。

那所有的指針類型都會有這個問題倦畅。

以 Objective-C 的 NSArray * 為例子遮糖,如果一個多線程操作這個數(shù)據(jù),會有兩個層級的并發(fā)問題:

  1. 指針本身
  2. 指針?biāo)赶虻膬?nèi)存

指針本身也是占用內(nèi)存的叠赐,并且一定是 8 個字節(jié)(64 位系統(tǒng))欲账。第二部分,指針?biāo)赶虻膬?nèi)存芭概,有可能非常大赛不,有可能也就 1 個字節(jié)。

所以考慮 NSArray * array 這個數(shù)據(jù)在進(jìn)行多線程操作的時候罢洲,必須分成兩部分來描述踢故,一個是 &array 這個指針本身,另一個則是它所指向的內(nèi)存 array惹苗。想象現(xiàn)在有兩塊內(nèi)存殿较,一塊是 8 字節(jié),一塊 n 字節(jié)桩蓉,8 字節(jié)里面放的值淋纲,就是 n 字節(jié)內(nèi)存的首地址。

如果用 atomic 修飾之后院究,會有什么影響帚戳?

從內(nèi)存的角度來解釋這個過程。atomic 其實(shí)修飾的是這個指針 &array儡首,與指針指向的第二部分 n 字節(jié)數(shù)據(jù)沒有任何關(guān)系片任,被 atomic 修飾之后,你不可能隨意去多線程操作這個 8 字節(jié)蔬胯,但是對 8 字節(jié)里面所指向的 n 字節(jié)沒有任何限制对供!

atomic 已經(jīng)完美的履行了它的指責(zé),你不可能對這個 8 字節(jié)進(jìn)行無序的多線程操作,這就夠了呀产场!有問題的是程序員鹅髓,程序員并未對 n 字節(jié)做任何的限制。

二京景、NSMutableArray 本身是線程不安全的

簡單來說窿冯,線程安全就是多個線程訪問同一段代碼,程序不會異常确徙、不 Crash醒串。而編寫線程安全的代碼主要依靠線程同步。

  1. 不使用 atomic 修飾屬性鄙皇。原因有二:

    • atomic 的內(nèi)存管理語義是原子性的芜赌,僅保證了屬性的 setter 和 getter 方法是原子性的、線程安全的伴逸,但是屬性的其他方法缠沈,如數(shù)組添加/移除元素等并不是原子操作,所以不能保證屬性是線程安全的错蝴。

    • atomic 雖然保證了 getter洲愤、setter 方法線程安全,但是付出的代價很大顷锰,執(zhí)行效率要比 nonatomic 慢很多倍(有說法是慢 10-20 倍)禽篱。

    總之:使用 nonatomic 修飾 NSMutableArray 對象就可以了,而使用鎖馍惹、dispatch_queue 來保證 NSMutableArray 對象的線程安全躺率。

  2. 打造線程安全的 NSMutableArray

    《Effective Objective-C 2.0》書中第 41 條:多用派發(fā)隊列,少用同步鎖中指出:使用“串行同步隊列”(serial synchronization queue)万矾,將讀取操作及寫入操作都安排在同一個隊列里悼吱,即可保證數(shù)據(jù)同步。而通過并發(fā)隊列良狈,結(jié)合GCD 的柵欄塊(barrier)來不僅實(shí)現(xiàn)數(shù)據(jù)同步線程安全后添,還比串行同步隊列方式更高效。

    GCD 的柵欄塊作用示意圖

    說明:柵欄塊單獨(dú)執(zhí)行薪丁,不能與其他塊并行遇西。直到當(dāng)前所有并發(fā)塊都執(zhí)行完畢,才會單獨(dú)執(zhí)行這個柵欄塊

    線程安全實(shí)現(xiàn)如下:

    @interface QSThreadSafeMutableArray()
    @property (nonatomic, strong) NSMutableArray * MDataArray;
    @property (nonatomic, strong) dispatch_queue_t MSyncQueue;
    @end
    
    @implementation QSThreadSafeMutableArray
    - (instancetype)initCommon
    {
        if (self = [super init]) {
            // %p 以 16 進(jìn)制的形式輸出內(nèi)存地址严嗜,附加前綴 0x
            NSString * uuid = [NSString stringWithFormat:@"com.jzp.array_%p", self];
            // 注意:_MSyncQueue 是并行隊列
            _MSyncQueue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT);
        }
        return self;
    }
    
    - (instancetype)init
    {
        if (self = [self initCommon]) {
            _MDataArray = [NSMutableArray array];
        }
        return self;
    }
    
    - (id)objectAtIndex:(NSUInteger)index
    {
        __block id obj;
        
        dispatch_sync(_MSyncQueue, ^{
            if (index < [_MDataArray count]) {
                obj = _MDataArray[index];
            }
        });
        return obj;
    }
    
    - (NSEnumerator *)objectEnumerator
    {
        __block NSEnumerator * enu;
        
        dispatch_sync( _MSyncQueue, ^{
            enu = [_MDataArray objectEnumerator];
        });
        return enu;
    }
    
    - (void)insertObject:(id)anObject atIndex:(NSUInteger)index
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if (anObject && index < [_MDataArray count]) {
                [_MDataArray insertObject:anObject atIndex:index];
            }
        });
    }
    
    - (void)addObject:(id)anObject
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if(anObject){
                [_MDataArray addObject:anObject];
            }
        });
    }
    
    - (void)removeObjectAtIndex:(NSUInteger)index
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if (index < [_MDataArray count]) {
                [_MDataArray removeObjectAtIndex:index];
            }
        });
    }
    
    - (void)removeLastObject
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            [_MDataArray removeLastObject];
        });
    }
    
    - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if (anObject && index < [_MDataArray count]) {
                [_MDataArray replaceObjectAtIndex:index withObject:anObject];
            }
        });
    }
    
    - (NSUInteger)indexOfObject:(id)anObject
    {
        __block NSUInteger index = NSNotFound;
        
        dispatch_sync( _MSyncQueue, ^{
            for (int i = 0; i < [_MDataArray count]; i ++) {
                if ([_MDataArray objectAtIndex:i] == anObject) {
                    index = i;
                    break;
                }
            }
        });
        return index;
    }
    
    - (void)dealloc
    {
        if (_MSyncQueue) {
            _MSyncQueue = NULL;
        }
    }
    
    @end
    

    說明 ①:使用 dispatch queue 實(shí)現(xiàn)線程同步粱檀;將同步與異步派發(fā)結(jié)合起來,可以實(shí)現(xiàn)與普通加鎖機(jī)制一樣的同步行為漫玄,又不會阻塞執(zhí)行異步派發(fā)的線程茄蚯;使用同步隊列及柵欄塊压彭,可以令同步行為更加高效。

    說明 ②:NSMutableDictionary 本身也是線程不安全的渗常,實(shí)現(xiàn)線程安全的 NSMutableDictionary 原理同線程安全的NSMutableArray壮不。(代碼見 QSUseCollectionDemo)

  3. 線程安全的 NSMutableArray 使用

    - (void)testQsMutableArray
    {
        _MSafeArray = [[QSThreadSafeMutableArray alloc] init];
        
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        for (NSInteger i = 0; i < 10; i++) {
            dispatch_async(queue, ^{
                NSString * str = [NSString stringWithFormat:@"數(shù)組%d", (int)i+1];
                [_MSafeArray addObject:str];
            });
        }
        
        sleep(1);
        
        NSEnumerator * enu = [_MSafeArray objectEnumerator];
        
        for (NSObject * object in enu) {
            NSLog(@"value: %@", object);
        }
    }
    

三、atomic 與 nonatomic 的區(qū)別

在默認(rèn)情況下皱碘,由編譯器生成的屬性的 set询一、get 方法會通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質(zhì)癌椿,則不需要同步鎖健蕊。

盡管沒有指明 atomic 的特質(zhì)(如果某屬性不具備 nonatomic 特質(zhì),那它就是"原子的"(atomic))如失,仍然可以在屬性特質(zhì)中寫明這一點(diǎn),編譯器是不會報錯的送粱。

一般 iOS 程序中褪贵,所有屬性都聲明為 nonatomic。這樣做的原因是:

  1. 在 iOS 中使用同步鎖的開銷比較大抗俄, 會帶來性能問題脆丁。

  2. 一般情況下并不要求屬性必須是"原子的",因為這并不能保證線程安全动雹。若要實(shí)現(xiàn)線程安全的操作槽卫,還需采用更為深層的鎖的機(jī)制。

一個線程在連續(xù)多次讀取某個屬性值的過程中有別的線程在同時改寫該值胰蝠,那么即便將屬性聲明為 atomic 也還是會讀取到不同的屬性值歼培。

因此,iOS 程序一般都會使用 nonatomic 屬性茸塞。但在 Mac OS X 程序時躲庄,使用 atomic 屬性通常都不會有性能瓶頸。

nonatomic 的實(shí)現(xiàn):

- (void)setImage:(UIImage *)image
{
    if (_image != image) {
        [_image release];
        _image = [image retain];
        ...
    }
}

- (UIImage *)image
{
    return _image;
}

atomic 的實(shí)現(xiàn):

- (void)setImage:(UIImage *)image
{
    @synchronized(self) {
        // 鎖
        if (_image != image) {
            [_image release];
            _image = [image retain];
            ...
        }
    }
}

- (UIImage *)image
{
    @synchronized(self) {
        return _image;
    }
}

@synchronized 的介紹:

The @synchronized directive is a convenient way to create mutex locks on the fly in Objective-C code. The @synchronized directive does what any other mutex lock would do—it prevents different threads from acquiring the same lock at the same time. In this case, however, you do not have to create the mutex or lock object directly. Instead, you simply use any Objective-C object as a lock token, as shown in the following example:

- (void)myMethod:(id)anObj
{

@synchronized(anObj) {

// Everything between the braces is protected by the @synchronized directive.

}

}

The object passed to the @synchronized directive is a unique identifier used to distinguish the protected block. If you execute the preceding method in two different threads, passing a different object for the anObj parameter on each thread, each would take its lock and continue processing without being blocked by the other. If you pass the same object in both cases, however, one of the threads would acquire the lock first and the other would block until the first thread completed the critical section.

As a precautionary measure, the @synchronized block implicitly adds an exception handler to the protected code. This handler automatically releases the mutex in the event that an exception is thrown. This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes.

For more information about the @synchronized directive, see The Objective-C Programming Language.

更準(zhǔn)確的說應(yīng)該是讀寫安全钾虐,但并不是線程安全的噪窘,因為別的線程還能進(jìn)行讀寫之外的其他操作。線程安全需要開發(fā)者自己來保證效扫。

四倔监、文章

清雨未盡時 & NSMutableArray使用中忽視的問題

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市菌仁,隨后出現(xiàn)的幾起案子浩习,更是在濱河造成了極大的恐慌,老刑警劉巖济丘,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘦锹,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)弯院,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門辱士,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人听绳,你說我怎么就攤上這事颂碘。” “怎么了椅挣?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵头岔,是天一觀的道長。 經(jīng)常有香客問我鼠证,道長峡竣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任量九,我火速辦了婚禮适掰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘荠列。我一直安慰自己类浪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布肌似。 她就那樣靜靜地躺著费就,像睡著了一般。 火紅的嫁衣襯著肌膚如雪川队。 梳的紋絲不亂的頭發(fā)上力细,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音固额,去河邊找鬼艳汽。 笑死,一個胖子當(dāng)著我的面吹牛对雪,可吹牛的內(nèi)容都是我干的河狐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瑟捣,長吁一口氣:“原來是場噩夢啊……” “哼馋艺!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起迈套,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤捐祠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后桑李,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踱蛀,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窿给,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了率拒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片崩泡。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖猬膨,靈堂內(nèi)的尸體忽然破棺而出角撞,到底是詐尸還是另有隱情,我是刑警寧澤勃痴,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布谒所,位于F島的核電站,受9級特大地震影響沛申,放射性物質(zhì)發(fā)生泄漏劣领。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一铁材、第九天 我趴在偏房一處隱蔽的房頂上張望尖淘。 院中可真熱鬧,春花似錦衫贬、人聲如沸德澈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缴守,卻和暖如春葬毫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背屡穗。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工贴捡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人村砂。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓烂斋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親础废。 傳聞我的和親對象是個殘疾皇子汛骂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容