关于ios:Objective-C中原子/非原子的证据

Evidence of atomic / nonatomic in Objective-C

在阅读了苹果公司的文档之后,我试图在Objective-C中证明一个属性的原子性或非原子性。为此,我创建了一个具有名字和姓氏的类人员。

人H

1
2
3
4
5
6
@interface Person : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;

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

人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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

在另一个类中,这里是我的AppDeaveT,我有一个非原子属性,它是人的实例。

1
@property (strong, nonatomic) Person *p;

在实现文件中,我创建了三个并发队列。在第一个队列中,我读取属性,在另外两个队列中,我写入不同的Person值。

据我所知,我可以在日志中输出BobFrost或JackSponce,因为我声明我的财产是非原子的。但事实并非如此。我不明白为什么。我是错过了什么还是误解了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (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对象。到当前代码运行时,对象已经存在,并且您不再更改它们的名称值,因此不可能出现争用条件或使用名称值进行部分写入。你只是在两个对象之间改变self.p,它们一旦被创建就不会改变。

也就是说,代码中不可预测的是,在任何时刻,person对象都将是self.p中的什么。您应该可以看到显示的值在Bob Sponce和Jack Frost之间不可预知地交替显示。

一个更好的测试应该是这样的:

(假设每个测试对象的x1和x2值始终保持不变。)

1
2
3
4
5
6
7
8
9
10
11
@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

然后像这样编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#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,给编辑我的代码的人:代码格式是一个风格和个人品味的问题。我在"奥尔曼缩进"上使用了一个变体。我欣赏拼写错误的更正,但我不喜欢K&R样式的缩进。不要把你的风格强加于我的代码。


属性为atomic意味着由读操作执行的所有操作以及由写操作执行的所有操作都是原子执行的。(这完全独立于两个独立属性之间的一致性,如您的示例中所示,这不能通过添加(atomic)来实现。)

这在两种情况下尤为重要:

  • 对于对象指针,存储新值时执行的隐式[_property release]; [newValue retain]; _property = newValue操作,以及加载值时发生的隐式value = _property; [value retain];

  • 不考虑保留/释放语义,实际值不能原子地加载/存储的大型数据类型。

  • 下面是一个例子,说明了两个潜在问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    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,对于对象属性,偶尔会出现exc-bad-access崩溃,并记录如下消息:

    AtomicTest[2172:57275] Latest object:
    objc[2172]: NSObject object 0x100c04a00 overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug

    对于数据结构,断言偶尔会失败:

    AtomicTest[2240:59304] *** Assertion failure in -[Producer logStatus], main.m:58
    AtomicTest[2240:59304] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'WRONG: 55937112^2 != 3128960610774769'

    (注意EDCOX1,6,3128960610774769的值实际上是559371132,而不是559371122。)

    使EDOCX1×2的特性而不是EDCOX1〔8〕避免了这两个问题,代价是执行速度稍慢。

    边注:即使在SWIFT中也会出现同样的问题,因为没有原子属性的概念:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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)")
    }


    From what I understand, I could have Bob Frost or Jack Sponge output in my log, since I declared my property as nonatomic. But that didn't happened. I don't understand why. Am I missing something or misunderstanding something ?

    如果你触发了比赛条件,就不会发生这种情况。几乎可以肯定的是,你会撞车,或者你会得到一些真正令人惊讶的东西。

    原子意味着你总是会得到一个一致的值,我的意思是"你在属性中实际放入的一个值"。如果没有原子,就有可能得到一个值,而这不是任何线程写的值。考虑一下这个程序,它必须根据32位体系结构编译(这也意味着必须禁用ARC,并且您需要声明您的IVARS才能在Mac上工作;或者您可以在32位iPhone上测试它)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 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)
    "
    , snapshot, value1, value2);
                }
            }
        }
        return 0;
    }

    如果你运行这个超过几秒钟,你会收到很多信息,比如:

    1
    2
    ***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代码(即,在属性之前):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @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中返回bar指针。这基本上是今天EDOCX1 1的实现。问题是内存管理不是线程安全的。考虑一下,如果你只调用一个线程上的EDCOX1,2,然后在另一个线程上调用GETTER。您将获得EDCOX1 3的旧值,该值已经被释放,并且可能已经被解除分配。因此,您可能正在查看无效内存,然后崩溃。

    一个常见的解决方案是retain/autorelease getter:

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

    这就确保了无论_something所指的是什么,至少在当前自动租赁池结束之前都会存在(如果您希望它超出这个范围,那么无论如何保留它都是您的责任)。这比普通的吸气剂要慢得多。atomic还通过确保在设置过程中没有人抓到get来解决这个问题。

    尽管在某些情况下,这可能很有价值,但如果您在多个队列中访问数据,那么几乎总是这样,atomic不够,而且速度很慢(至少以前是这样;我没有分析最近的版本,因为我从未使用atomic)。如果您只需要单个属性原子性,那么gcd访问器通常更好。如果您需要一个完全原子化的事务(您经常这样做),那么gcd访问器也可以很容易地适应它。

    也许最好的讨论是bbum的博客:http://www.friday.com/bbum/2008/01/13/objective-c-atomic-properties-threading-andor-custom-settgetter/。简短的回答是,很少有atomic实际上有用。如果你认为你需要atomic,你通常需要的比它给你的更多,而且通常可以用gcd的附加器以更便宜的价格得到它。

    atomic违约是苹果在objc2中犯下的一个重大错误。