正文
問題列表
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)導致的事故