iOS property的多線程問題解析

正文

問題列表

1、以下這段代碼,在主線程執行會輸出什么?

// 屬性
@property (nonatomic, strong) NSArray *myNumberArr; 

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, self.myNumberArr.count); 
    }); 
} 

2、稍作修改,以下代碼在主線程執行會輸出什么?

// 屬性
@property (atomic, strong) NSArray *myNumberArr; 

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        _myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, _myNumberArr.count); 
    }); 
} 

3、換了個類型,以下代碼在主線程執行會輸出什么?

// 屬性
@property (nonatomic, strong) NSNumber *myNumber; 

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumber = @(i);
        NSLog(@"%d, count:%d", i, self.myNumber.intValue); 
    }); 
} 

問題分析

題目1
// 屬性
@property (nonatomic, strong) NSArray *myNumberArr; 

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, self.myNumberArr.count); 
    }); 
} 

先解析提供的要素:
a.nonatomic的NSArray屬性;
b.異步執行,gcd并發隊列;
c.多個block,對myNumberArr的多次讀寫操作;

由a+b+c組成了一個多線程訪問nonatomic屬性的方法,如果直接運行會遇到下面的問題:

題目2

// 屬性
@property (atomic, strong) NSArray *myNumberArr; 

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        _myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, _myNumberArr.count); 
    }); 
} 

myNumberArr的屬性變為了atomic;
屬性的訪問,沒有用.myNumberArr的getter方式,而是直接使用_myNumberArr訪問;

如果直接運行,同樣會遇到下面的問題:


題目3

// 屬性
@property (nonatomic, strong) NSNumber *myNumber; 

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumber = @(i);
        NSLog(@"%d, count:%d", i, self.myNumber.intValue); 
    }); 
} 

與題目1類似:
a.nonatomic的NSNumber屬性;
b.異步執行,gcd并發隊列;
c.多個block,對myNumber的多次讀寫操作;

由題目1的經驗,由a+b+c組成了一個多線程訪問nonatomic屬性myNumber的方法,預期直接運行會遇到相同的多線程問題。但實際上是可以正常跑完,即使多嘗試幾次。

問題延伸

多線程問題出現原因

為了更好理解多線程讀寫屬性的理解,我們以題目1為樣例,假設其代碼在-viewDidLoad方法。

// 運行代碼
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        NSArray *arr = @[[NSString stringWithFormat:@"%d", i]];
        self.myNumberArr = arr; 
    }); 
} 

上面的代碼其實是跑在ARC的環境下,而OC實現ARC就是編譯時添加retain和release的方法。

我們將上面的ARC代碼轉成更原始的代碼,得到更接近真實運行的代碼。

轉換方式:將ViewController.m的代碼編譯,得到ViewController.o文件,再進行還原得到類似的MRC代碼。

比如說block中的代碼會生成:

根據上圖,給self.myNumberArr賦值的var_28已經進行過retain操作。

打開setMyNumberArr:方法,我們知道最終賦值的操作是通過objc_storeStrong來執行,這個方法如下:

void objc_storeStrong(id *object, id value) {
  id oldValue = *object;
  value = [value retain];
  *object = value;
  [oldValue release];
}

當我們給self.myNumberArr賦值時,除了需要retain傳進來的值,還需要將self.myNumberArr原來的值進行release,否則賦值之后原來的self.myNumberArr值將成為野指針。

當我們有多個線程執行self.myNumberArr = arr,也就是會有多個線程同時跑到objc_storeStrong函數。
假設線程1和線程2同時運行objc_storeStrong,他們會同時拿到oldValue,此時retainCount都為1;當2個線程執行完賦值操作,都會對oldValue進行release操作,此時就會觸發對象的重復release,造成崩潰。

nonatomic和atomic的區別

我們把myNumberArr的生命的nonatomic改成atomic,再試試看看生成的代碼。

// 屬性
@property (atomic, strong) NSArray *myNumberArr; 

結果如下:

objc_setProperty_atomic的代碼如下:
(注意看下面if(atomic)的分支)

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:NULL];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:NULL];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
        _spin_lock(slotlock);
        oldValue = *slot;
        *slot = newValue;        
        _spin_unlock(slotlock);
    }

    objc_release(oldValue);
}

我們知道了atomic的屬性在setter的時候,會通過加鎖來保證賦值操作的原子性。

這樣也解釋了題目2的時候,為什么聲明了atomic,但是通過_myNumberArr屬性去操作會發生多線程問題。因為用下劃線_myNumberArr訪問屬性時,不會經過getter/setter。

NSArray的多線程問題

有一個經典問題:NSArray是否為線程安全類,能否用atomic修飾NSArray屬性保證屬性的讀寫線程安全嗎?
根據前面的分析,我們可以知道atomic可以保證getter/setter在使用的時候,不會出現多線程問題;
再根據官網的資料,我們知道NSArray是Thread Safety。
綜上,答案是:NSArray是線程安全類,可以用atomic修飾NSArray屬性保證屬性的讀寫線程安全。

但是,是否涉及NSArray的操作,都不需要考慮多線程的問題?
看下面的一段代碼:

@property (atomic, strong) NSArray<NSMutableDictionary *> *myAtomicArr;

// 運行代碼
for (int i = 0; i < 1000; ++i) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if ([self.myAtomicArr[0] objectForKey:@"testKey"]) {
            [self.myAtomicArr[0] removeObjectForKey:@"testKey"];
        }
        else {
            self.myAtomicArr[0][@"testKey"] = @"testStr";
        }
    });
}

通過前面的經驗,我們容易知道這種代碼會出現多線程問題。
我們能保證NSArray類的線程安全,但是無法保證NSArray內的屬性操作是線程安全,所以在使用NSArray時,仍需要小心多線程問題。

思考題??:

為什么題目3可以正常運行?(答案見附錄最后一篇)

總結

多線程的問題有很多場景,這里僅針對屬性的多線程讀寫這個case進行分析,對多線程問題建立一個基礎的認知。

附錄

Thread Safety Summary
objc_storeStrong
objc_setProperty_atomic
一次標簽指針(Tagged Pointer)導致的事故

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