從一道網(wǎng)易面試題淺談OC線程安全

今天去網(wǎng)易面試,面試官出了一道面試題,下面代碼會(huì)發(fā)生什么問(wèn)題?

@property (nonatomic, strong) NSString *target;
//....

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
    });
}

當(dāng)時(shí)我把自定義的隊(duì)列看成了串行隊(duì)列,然后回答:“沒(méi)錯(cuò)呀”。后來(lái)一運(yùn)行崩潰了……

面試后,我就仔細(xì)回想,敲了Demo,看看崩潰原因是啥。

正好試試小伙伴給我介紹的調(diào)試野指針的方法,XCode7以上才有的Address Sanitizer

打開(kāi)后發(fā)現(xiàn)是經(jīng)典的EXC_BAD_ACCESS錯(cuò)誤,以我淺薄的經(jīng)驗(yàn)來(lái)看,這種一般是對(duì)一個(gè)已釋放的內(nèi)存的對(duì)象再次發(fā)送消息出現(xiàn)的。

屏幕快照 2017-08-25 上午1.55.50

再看看崩潰堆棧

屏幕快照 2017-08-25 上午1.53.22

噢,看來(lái)是對(duì)已釋放的對(duì)象再次發(fā)送了release信息。

我又留意到,這個(gè)對(duì)象是Strong修飾的,或許可以從Strong和Setter方法的源碼入手看看。

下面源碼基于Runtime-709分析,首先找到屬性設(shè)置方法。

//objc_class.mm
void object_setIvar(id obj, Ivar ivar, id value)
{
    return _object_setIvar(obj, ivar, value, false /*not strong default*/);
}


static ALWAYS_INLINE 
void _object_setIvar(id obj, Ivar ivar, id value, bool assumeStrong)
{
    //判斷是否是TaggedPointer
    if (!obj  ||  !ivar  ||  obj->isTaggedPointer()) return;

    ptrdiff_t offset;
    objc_ivar_memory_management_t memoryManagement;
    //找對(duì)應(yīng)的內(nèi)存管理語(yǔ)義和屬性偏移值
    _class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);

    //如果找不到默認(rèn)是否為Strong,不然為unsafe_unretained
    if (memoryManagement == objc_ivar_memoryUnknown) {
        if (assumeStrong) memoryManagement = objc_ivar_memoryStrong;
        else memoryManagement = objc_ivar_memoryUnretained;
    }

    //根據(jù)偏移值找到屬性對(duì)應(yīng)位置
    id *location = (id *)((char *)obj + offset);
    
    //判斷不同的內(nèi)存管理語(yǔ)義,調(diào)用方法
    switch (memoryManagement) {
    case objc_ivar_memoryWeak:       objc_storeWeak(location, value); break;
    case objc_ivar_memoryStrong:     objc_storeStrong(location, value); break;
    case objc_ivar_memoryUnretained: *location = value; break;
    case objc_ivar_memoryUnknown:    _objc_fatal("impossible");
    }
}
//NSObject.mm
void
objc_storeStrong(id *location, id obj)
{   
    //如果新值指針和舊值一樣,則不更新,直接return
    id prev = *location;
    if (obj == prev) {
        return;
    }
    //先對(duì)新值retain
    objc_retain(obj);
    //再賦值
    *location = obj;
    //最后對(duì)舊值release
    objc_release(prev);
}

那么他的Setter方法在MRC上就相當(dāng)于

- (void)setTarget:(NSString *)target {
    if (target == _target) return;
    id pre = _target;
    [target retain];//1.先保留新值
    _target = target;//2.再進(jìn)行賦值
    [pre release];//3.釋放舊值
}

什么時(shí)候會(huì)導(dǎo)致過(guò)多調(diào)用release呢?注意這是個(gè)并發(fā)隊(duì)列+異步。

那么假如并發(fā)隊(duì)列里調(diào)度的線程A執(zhí)行到步驟1,還沒(méi)到步驟2時(shí),線程B執(zhí)行到步驟3,那么當(dāng)線程A再執(zhí)行步驟3時(shí),舊值就會(huì)被過(guò)度釋放,導(dǎo)致向已釋放內(nèi)存對(duì)象發(fā)送消息而崩潰。

后來(lái)我想怎么可以修改這段代碼變?yōu)椴槐罎⒌哪兀?/p>

1.使用串行隊(duì)列

將set方法改成在串行隊(duì)列中執(zhí)行就行,這樣即使異步,但所有block操作追加在隊(duì)列最后依次執(zhí)行。

2. 使用atomic

atomic關(guān)鍵字相當(dāng)于在setter方法加鎖,這樣每次執(zhí)行setter都是線程安全的,但這只是單獨(dú)針對(duì)setter方法而言的狹義的線程安全。

3.使用weak關(guān)鍵字

weak的setter沒(méi)有保留新值或者保留舊值的操作,所以不會(huì)引發(fā)重復(fù)釋放。當(dāng)然這個(gè)時(shí)候要看具體情況能否使用weak,可能值并不是所需要的值。

4.使用Tagged Pointer

Tagged Pointer是蘋(píng)果在64位系統(tǒng)引入的內(nèi)存技術(shù)。簡(jiǎn)單來(lái)說(shuō)就是對(duì)于NSString(內(nèi)存小于60位的字符串)或NSNumber(小于2^31),64位的指針有8個(gè)字節(jié),完全可以直接用這個(gè)空間來(lái)直接表示值,這樣的話其實(shí)會(huì)將NSString和NSNumber對(duì)象由一個(gè)指針轉(zhuǎn)換成一個(gè)值類(lèi)型,而值類(lèi)型的setter和getter又是原子的,從而線程安全。

比如上述代碼的字符串改短一些,就不會(huì)崩潰了。

從而我們可以總結(jié)到,線程安全有以下幾種方法:

  • 單線程串行訪問(wèn)
  • 訪問(wèn)加鎖
  • 使用不進(jìn)行額外操作的關(guān)鍵字(weak)
  • 使用值類(lèi)型

然而這只是保證了基本的線程安全(不崩潰),若是需要保證訪問(wèn)出符合預(yù)期的數(shù)據(jù),則需要采用GCD的barrier或者自己在合適的時(shí)機(jī)加鎖。

最后

有任何問(wèn)題歡迎評(píng)論私信
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz

謝謝觀看

參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 下面代碼會(huì)發(fā)生什么問(wèn)題?@property (nonatomic, strong) NSString *targe...
    怎樣m閱讀 278評(píng)論 0 0
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛(ài)閱讀 2,013評(píng)論 0 7
  • ———————————————回答好下面的足夠了---------------------------------...
    恒愛(ài)DE問(wèn)候閱讀 1,759評(píng)論 0 4
  • 多線程、特別是NSOperation 和 GCD 的內(nèi)部原理。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場(chǎng)景。SDWebImage的原...
    LZM輪回閱讀 2,039評(píng)論 0 12
  • 史上最全的iOS面試題及答案 iOS面試小貼士———————————————回答好下面的足夠了----------...
    Style_偉閱讀 2,430評(píng)論 0 35