繁体   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