簡體   English   中英

Objective-C中原子/非原子的證據

[英]Evidence of atomic / nonatomic in Objective-C

在閱讀Apple的文檔后 ,我嘗試在Objective-C中提供屬性的原子性或非原子性。 為此,我創建了一個具有名和姓的Person。

Person.h

@interface Person : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

- (instancetype)initWithFirstName:(NSString *)fn lastName:(NSString *)ln;
@end

Person.m

@implementation Person

- (instancetype)initWithFirstName:(NSString *)fn lastName:(NSString *)ln {
    if (self = [super init]) {
        self.firstName = fn;
        self.lastName = ln;
    }
    return self;
}

- (NSString *)description {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

@end

在另一個類中,我的AppDelegate,我有一個非原子屬性,它是Person的一個實例。

@property (strong, nonatomic) Person *p;

在實現文件中,我創建了三個並發隊列。 在第一個隊列中,我讀取了屬性,在另外兩個隊列中,我寫了不同的person值。

根據我的理解,我可以在我的日志中輸出Bob FrostJack Sponge ,因為我聲明我的屬性是非原子的 但那並沒有發生。 我不明白為什么。 我錯過了什么或誤解了什么嗎?

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.

    Person *bob = [[Person alloc] initWithFirstName:@"Bob" lastName:@"Sponge"];
    Person *jack = [[Person alloc] initWithFirstName:@"Jack" lastName:@"Frost"];
    self.p = bob;

    dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue1, ^{
        while (YES) {
            NSLog(@"%@", self.p);
        }
    });

    dispatch_async(queue2, ^{
        while (YES) {
            self.p = bob;
        }
    });

    dispatch_async(queue3, ^{
        while (YES) {
            self.p = jack;
        }
    });

    return YES;
}

具有非原子屬性使得部分寫入的可能性成為可能,但決不是確定的。

在Person類中,設置名字和姓氏的唯一方法是在init方法中,然后設置第一個名稱,然后立即設置姓氏。 設置名字和姓氏將非常接近彼此,很少有機會讓另一個線程在操作之間弄亂。

此外,在運行並發操作之前,在主線程中創建Person對象。 當您的當前代碼運行時,對象已經存在,您不再更改其名稱值,因此不存在競爭條件或具有名稱值的部分寫入的可能性。 您只是在2個對象之間更改self.p,這些對象在創建后不會更改。

也就是說,你的代碼無法預測的是什么人物在任何時刻都會在self.p中出現。 您應該看到Bob Sponge和Jack Frost之間顯示的值無法預測。

更好的測試是這樣的:

(假設每個TestObject的x1和x2值應始終保持不變。)

@interface TestObject : NSObject
@property (nonatomic, assign) int x1;
@property (nonatomic, assign) int x2;
@end

@interface AppDelegate
@property (nonatomic, strong) TestObject *thing1;
@property (nonatomic, strong) TestObject *thing2;
@property (nonatomic, strong) NSTimer *aTimer;
@property (nonatomic, strong) NSTimer *secondTimer;
@end

然后像這樣的代碼:

#include <stdlib.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
  dispatch_queue_t queue1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
  dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);

  self.thing1 = [[TestObject alloc] init];
  self.thing2 = [[TestObject alloc] init];

  dispatch_async(queue1, ^
  {
    for (int x = 0; x < 100; x++) 
    {
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      int thing1Val = arc4random_uniform(10000);
      int thing2Val = arc4random_uniform(10000);
      _thing1.x1 = thing1Val;
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x1 = thing2Val;
      _thing1.x2 = thing1Val; //thing1's x1 and x2 should now match
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x2 = thing2Val; //And now thing2's x1 and x2 should also both match
    }
  });


  //Do the same thing on queue2
  dispatch_async(queue2, ^
  {
    for (int x = 0; x < 100; x++) 
    {
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      int thing1Val = arc4random_uniform(10000);
      int thing2Val = arc4random_uniform(10000);
      _thing1.x1 = thing1Val;
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x1 = thing2Val;
      _thing1.x2 = thing1Val; //thing1's x1 and x2 should now match
      usleep(arc4random_uniform(50000)); //sleep for 0 to 50k microseconds
      _thing2.x2 = thing2Val; //And now thing2's x1 and x2 should also both match
    }
  });

  //Log the values in thing1 and thing2 every .1 second
  self.aTimer = [NSTimer scheduledTimerWithTimeInterval:.1
    target:self
    selector:@selector(logThings:)
    userInfo:nil
    repeats:YES];

  //After 5 seconds, kill the timer.
  self.secondTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
    target:self
    selector:@selector(stopRepeatingTimer:)
    userInfo:nil
    repeats:NO];
  return YES;
}

- (void)stopRepeatingTimer:(NSTimer *)timer 
{
  [self.aTimer invalidate];
}

- (void)logThings:(NSTimer *)timer 
{
  NSString *equalString;
  if (_thing1.x1 == _thing1.x2) 
  {
    equalString = @"equal";
  }
    else 
  {
    equalString = @"not equal";
  }
  NSLog(@"%@ : thing1.x1 = %d, thing1.x2 = %d", 
    equalString, 
    _thing1.x1, 
    _thing1.x2);

  if (_thing2.x1 == _thing2.x2) 
    {
      equalString = @"equal";
    }
  else 
    {
      equalString = @"not equal";
    }
  NSLog(@"%@ : thing2.x1 = %d, thing2.x2 = %d", 
    equalString, 
    _thing2.x1, 
    _thing2.x2);
 }

在上面的代碼中,每個隊列都會創建一系列隨機值,並將一對對象的x1和x2屬性設置為重復循環中的隨機值。 它延遲了設置每個對象的x1和x2屬性之間的小的隨機間隔。 該延遲模擬后台任務需要一些時間來完成應該是原子的工作。 它還引入了一個窗口,其中另一個線程可以在當前線程能夠設置第二個值之前更改第二個值。

如果你運行上面的代碼,你幾乎肯定會發現thing1和thing2的x1和x2值有時是不同的。

上面的代碼對原子屬性沒有幫助。 您需要在設置每個對象的x1和x2屬性之間聲明某種鎖(可能使用@synchronized指令)。

(請注意,我在論壇編輯器中將上面的代碼組合在一起。我沒有嘗試編譯它,更不用說調試它了。毫無疑問有一些錯別字。)

(注2,編輯我的代碼的人:代碼格式是風格和個人品味的問題。我使用“Allman縮進”的變體。我欣賞錯別字修正,但我鄙視K&R風格縮進。不要強加我的代碼風格。

atomic屬性意味着讀取執行的所有操作以及寫入執行的所有操作都是以原子方式完成的。 (這完全獨立於兩個單獨屬性之間的一致性,如在您的示例中,僅通過添加(atomic)無法實現。)

這在兩種情況下尤為重要:

  1. 對於對象指針,隱式[_property release]; [newValue retain]; _property = newValue [_property release]; [newValue retain]; _property = newValue [_property release]; [newValue retain]; _property = newValue ARC在存儲新值時執行的[_property release]; [newValue retain]; _property = newValue操作,隱含value = _property; [value retain]; value = _property; [value retain]; 加載值時會發生這種情況。

  2. 無論保留/釋放語義如何,其實際值都無法以原子方式加載/存儲的大型數據類型。

這是一個說明兩個潛在問題的例子:

typedef struct {
    NSUInteger x;
    NSUInteger xSquared;  // cached value of x*x
} Data;


@interface Producer : NSObject

@property (nonatomic) Data latestData;
@property (nonatomic) NSObject *latestObject;

@end


@implementation Producer

- (void)startProducing
{
    // Produce new Data structs.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (NSUInteger x = 0; x < NSUIntegerMax; x++) {
            Data newData;
            newData.x = x;
            newData.xSquared = x * x;

            // Since the Data struct is too large for a single store,
            // the setter actually updates the two fields separately.
            self.latestData = newData;
        }
    });

    // Produce new objects.
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (true) {
            // Release the previous value; retain the new value.
            self.latestObject = [NSObject new];
        }
    });

    [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(logStatus) userInfo:nil repeats:YES];
}

- (void)logStatus
{
    // Implicitly retain the current object for our own uses.
    NSObject *o = self.latestObject;
    NSLog(@"Latest object: %@", o);

    // Validate the consistency of the data.
    Data latest = self.latestData;
    NSAssert(latest.x * latest.x == latest.xSquared, @"WRONG: %lu^2 != %lu", latest.x, latest.xSquared);
    NSLog(@"Latest data: %lu^2 = %lu", latest.x, latest.xSquared);
}

@end



int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[Producer new] startProducing];
        [[NSRunLoop mainRunLoop] run];
    }
    return 0;
}

對於nonatomic ,對於object屬性,偶爾會出現EXC_BAD_ACCESS崩潰,並記錄如下消息:

AtomicTest [2172:57275]最新對象:<NSObject:0x100c04a00>
objc [2172]:NSObject對象0x100c04a00在已經解除分配時被過度釋放; 打破objc_overrelease_during_dealloc_error進行調試

對於Data結構,斷言偶爾會失敗:

AtomicTest [2240:59304] ***斷言失敗 - [Producer logStatus],main.m:58
AtomicTest [2240:59304] ***由於未捕獲的異常'NSInternalInconsistencyException'而終止應用程序,原因:'錯誤:55937112 ^ 2!= 3128960610774769'

(請注意, xSquared的值xSquared實際上是55937113 2而不是55937112 2.

使屬性(atomic)而不是(nonatomic)避免了這兩個問題,代價是執行速度稍慢。


旁注:即使在Swift中也會出現同樣的問題,因為沒有原子屬性的概念:

class Object { }
var obj = Object()

dispatch_async(dispatch_get_global_queue(0, 0)) {
    while true {
        obj = Object()
    }
}

while true {
    // This sometimes crashes, and sometimes deadlocks
    let o = obj
    print("Current object: \(o)")
}

根據我的理解,我可以在我的日志中輸出Bob Frost或Jack Sponge,因為我聲明我的屬性是非原子的。 但那並沒有發生。 我不明白為什么。 我錯過了什么或誤解了什么嗎?

如果你觸發了競爭條件,那就不會發生這種情況。 幾乎可以肯定的是,你會崩潰,或者你會得到一些非常令人驚訝的東西。

原子意味着您將始終獲得一致的價值,我的意思是“您實際投入房產的價值”。 如果沒有atomicy,就有可能得到一個不是任何線程所寫的值。 考慮一下這個程序,它必須針對32位架構進行編譯(這也意味着必須禁用ARC,你需要聲明你的ivars才能在Mac上運行它;或者你可以在32位iPhone上測試它) 。

// clang -arch i386 -framework Foundation atomic.m -o atomic ; ./atomic
#import <Foundation/Foundation.h>

@interface MyObject : NSObject {
    long long i;
}
@property (nonatomic) long long i;
@end

@implementation MyObject
@synthesize i;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        dispatch_queue_t queue2 = dispatch_queue_create("queue2", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queue3 = dispatch_queue_create("queue3", DISPATCH_QUEUE_CONCURRENT);

        MyObject *obj = [MyObject new];

        long long value1 = 0;
        long long value2 = LLONG_MAX;

        dispatch_async(queue2, ^{
            while (YES) {
                obj.i = value1;
            }
        });

        dispatch_async(queue3, ^{
            while (YES) {
                obj.i = value2;
            }
        });
        while (YES) {
            long long snapshot = obj.i;
            if (snapshot != value1 && snapshot != value2) {
                printf("***PANIC*** Got %lld (not %lld or %lld)\n", snapshot, value1, value2);
            }
        }
    }
    return 0;
}

如果你運行這個超過幾秒鍾,你將收到許多消息,如:

***PANIC*** Got 4294967295 (not 0 or 9223372036854775807)
***PANIC*** Got 9223372032559808512 (not 0 or 9223372036854775807)

你會注意到4294967295和9223372032559808512都沒有出現在程序中的任何地方。 它們如何出現在輸出中? 因為我正在使用32位代碼編寫64位數字。 沒有單個機器指令可以同時寫入所有64位。 上半部分將編寫,然后另一半。 如果另一個隊列同時寫入,則可以使用一次寫入的前32位和另一次寫入的底部32位。 atomic通過鎖定內存直到它寫入所有單詞來防止這種情況。

對象可能會出現另一個問題。 在ARC之前它特別成問題,但仍然可能發生。 考慮以下非常常見的ObjC-1代碼(即在屬性之前):

@interface MyObject : NSObject {
    id _something;
}
- (id)something;
- (void)setSomething:(id)newSomething;
@end

@implementation MyObject

- (id)something {
    return _something;
}

- (void)setSomething:(id)newSomething {
    [newSomething retain];
    [_something release];
    _something = newSomething;
}

@end

這是編寫訪問器的一種非常常見的方法。 在設置期間處理保留 - 新/釋放舊。 在get期間返回條形指針。 這基本上是今天nonatomic的實現。 問題是內存管理不是線程安全的。 考慮一下你是否剛剛在一個線程上調用[_something release] ,然后在另一個線程上調用getter。 你會得到_something的舊值,它已經被釋放,並且可能已經被釋放了。 所以你可能會看到無效的內存,你會崩潰。

一個常見的解決方案是retain / autorelease getter:

- (id)something {
    return [[_something retain] autorelease];
}

這取得了一定的,無論_something指出,至少會存在,直到目前的自動釋放池的結束(如果你想它超出了,這是你的責任,反正把它保留下來)。 這比瑣碎的吸氣劑慢得多。 atomic也通過確保在設置過程中沒有人抓住來解決這個問題。

所有這一切,雖然在少數情況下,這可能是有價值的,幾乎總是如果您訪問多個隊列中的數據, atomic是不夠的,並且無論如何都很慢(至少它曾經是;我沒有描述最近版本,因為我從不使用atomic )。 如果你想要的只是單屬性atomicy, GCD訪問器通常更好。 如果您需要完全原子事務(您經常這樣做),那么GCD訪問器也可以很容易地適應它。

可能最好的討論是bbum的博客文章: http ://www.friday.com/bbum/2008/01/13/objectivce-c-atomic-properties-threading-andor-custom-settergetter/。 簡短的回答是, atomic實際上很有用。 如果你認為你需要atomic ,你通常需要比它更多的東西,並且通常可以使用GCD訪問器以更便宜的價格獲得它。

atomic為默認值是Apple在ObjC2中犯下的重大錯誤之一。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM