Objective C原子属性线程安全

时间:2017-07-25 06:21:48

标签: objective-c multithreading atomic

我已经阅读了几篇关于原子的帖子并编写了一个演示来验证线程是否安全,就像这样

@property(atomic,assign) NSInteger sum;

//然后执行此操作

for (NSInteger i = 0; i<1000; i++) {
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        self.sum++;
    });

}

制作物业&#34;总和&#34;作为原子财产;并启动1000个并发线程来添加一个;

我虽然结果将是1000,但它不是,如果我添加一个NSLock来包装self.sum ++,结果是1000;

有谁帮我解释一下?

3 个答案:

答案 0 :(得分:4)

这有几层。

首先,声明的属性大多只是声明存取方法的捷径。编译器合成属性,默认情况下,如果您不提供自己的实现,则定义这些方法和实例变量来支持属性。

所以,这个:

@property(atomic,assign) NSInteger sum;

基本上就是这样:

- (NSInteger) sum;
- (void) setSum:(NSInteger)value;

该属性的合成产生一个实例变量和这些方法的实现:

@implementation ...
{
    NSUInteger _sum;
}

- (NSInteger) sum
{
    // ...
}
- (void) setSum:(NSInteger)value
{
    // ...
}

对于原子属性,-sum-setSum:的实现都保证运行,使得两者都不会中断另一个。拨打-sum同时发生&#34;同时&#34;通过调用-setSum:将返回-setSum:之前的值或其之后的值,但不会返回部分修改过的frankenstein值或任何临时值。同样,对-setSum:的两个同时调用将导致_sum具有来自这些调用中的一个或另一个的值,但绝不会有某些混合或临时值。这就是两个电话似乎按照严格的顺序发生的,无论是A还是B,B然后是A,任意。

对于具有复合类型的属性(例如NSRect),这更容易理解。设置该属性的两个线程永远不会导致,例如,来自一个线程的origin和来自另一个线程的size被存储。一个或另一个将赢得&#34;而矩形将是连贯的。同样,调用getter的线程永远不会看到混合值,即使它与调用setter的调用同时发生。

接下来,使用点语法(例如self.sum)访问属性实际上只是调用访问器的快捷方式。因为只有get和set访问器,而不是任何&#34;增量&#34;访问者,像self.sum++;这样的语句需要分别执行这两个操作:

[self setSum:[self sum] + 1];

因此,您的语句首先涉及调用-sum,然后调用每个线程-setSum:。没有什么可以确保其他线程不能相互交错。该属性的原子性并不能阻止它。也就是说,线程A可以从其调用-sum获得值5,线程B也可以从其调用-sum获得值5,每个可以计算6作为新值,然后它们都是使用值6调用-setSum:。因此,两个线程将具有&#34;递增&#34;该财产,但它只会增加1。

简而言之,原子性不是线程安全的。认为它是一个概念上的错误。它只是原子性。它确实防止了当多个线程同时访问同一属性但不是所有类型时可能发生的一种损坏。

答案 1 :(得分:0)

试试这个:

dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i<1000; i++) {
    dispatch_async(queue, ^{
       NSLock *aLock = [[NSLock alloc] init];
       [aLock lock];
       self.sum++;
       [aLock unlock];
    });
}

答案 2 :(得分:0)

另一种方法是在块中将属性设置为i的值,然后立即将该属性的值分配给另一个变量并检查它们是否相等。如果不是,请显示它们的值,期望值和线程号。我现在正在与您现在进行的测试中一样。

self.data.intPropertyCheck = myIndex;
NSInteger capturedPropertyValue = self.data.intPropertyCheck;

在使用dispatch_async时,即使在使用DISPATCH_QUEUE_CONCURRENT队列时,我也没有遇到一种防止一个线程踩踏该属性的方法。

如果该队列是主队列,则它实际上是一个同步队列,但这会阻塞主线程。

由于这会创建许多异步块,因此我将i更改为myIndex,为线程添加了一个名称,并将这些值添加到了日志记录中,以便您可以轻松地了解正在发生的事情。控制台,并可以搜索索引号。 这是我的for循环。

int iterations;
iterations = 1000;

...

for (int myIndex = 0; myIndex < iterations; myIndex++ ) {
    NSLog(@"Issuing test: %d", myIndex);
    // dispatch_async(queue, ^{
    dispatch_async(queue, ^{
        NSThread *myThread = [NSThread currentThread];
        myThread.name = [NSString stringWithFormat:@"Operation - %d", myIndex];
        
        self.data.intPropertyCheck = myIndex;
        NSInteger capturedPropertyValue = self.data.intPropertyCheck;
        BOOL success = self.data.intPropertyCheck == myIndex; // By the time we get here, the value from the previous line could have already changed.
       
        NSLog(@"Test# %d. Expected value: %d", myIndex, myIndex);
        NSLog(@"Test# %d. Actual value: %ld", myIndex, (long)self.data.intPropertyCheck); // This will occasionally show a different value. Why?  Many threads attempting to change the value at once. Even if it is atomic.
        if (success) {
            NSLog(@"Test# %d. Test passed.", myIndex);
        } else {
            NSLog(@"#### Test %d ––––––––––––––––––––––––––––– error start", myIndex);
            NSLog(@"#### Test %d failed. Values do not match.", myIndex);
            NSLog(@"#### Test %d failed. Values do not match. Thread: %@", myIndex, myThread.name);
            NSLog(@"#### Test %d failed. Expected value: %d", myIndex, myIndex);
            NSLog(@"#### Test %d failed. Captured value: %ld. Current value: %ld. Expected value: %d", myIndex, (long)capturedPropertyValue, (long)self.data.intPropertyCheck, myIndex);
             NSLog(@"#### Test %d ––––––––––––––––––––––––––––– error end", myIndex);
            
            nil;
            nil;
            NSLog(@"");
            
          //  NSAssert(success, @"#### Test %d failed. Values do not match.", myIndex);
        }
    });
}

包含了Nil,因此您可以根据需要在其上设置断点。