关于多线程:Objective C原子属性线程安全

Objective C atomic property thread safe

我读了几篇关于原子的文章,并编写了一个演示来验证线程安全,就像这样

1
@property(atomic,assign) NSInteger sum;

/然后这样做

1
2
3
4
5
6
7
for (NSInteger i = 0; i<1000; i++) {
    dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        self.sum++;
    });

}

将属性"sum"设置为原子属性;启动1000个并发线程添加一个;

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

有人帮我解释这个吗?


这有几个层次。

首先,声明的属性主要只是声明访问器方法的快捷方式。如果不提供自己的实现,则默认情况下会发生属性的编译器合成,定义这些方法和实例变量以支持属性。

所以,这个:

1
@property(atomic,assign) NSInteger sum;

基本上就是这样:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation ...
{
    NSUInteger _sum;
}

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

对于一个原子属性,-sum-setSum:的实现都被保证能够运行,使两者看起来都不能中断对方。对-sum的调用与对-setSum:的调用"同时"发生时,将返回-setSum:之前的值或之后的值,但决不会返回部分修改的Frankenstein值或任何中间值。同样,对-setSum:的两个同时调用将导致_sum具有这些调用中的一个或另一个调用的值,但不具有某些混合或中间值。这两个电话似乎是按照严格的顺序发生的,要么是A,要么是B,然后是A,任意地。

对于具有复合类型的属性,如NSRect,这更容易理解。设置该属性的两个线程永远不会导致一个线程的origin和另一个正在存储的线程的size。其中一个或另一个将"获胜",直肠将是连贯的。同样,调用getter的线程将永远不会看到混合值,即使它与对setter的调用同时发生。

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

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

因此,您的语句首先涉及一个调用-sum,然后对每个线程调用EDCOX1(1)。没有什么可以确保其他线程不能互相交错它们的操作。财产的原子性并不能阻止它。也就是说,线程A可以从它的调用到EDCOX1,0的值得到5,线程B也可以从其调用到EDCOX1,0的值中得到值5,每个都可以计算6作为新值,然后它们都调用EDCOX1与1的值6。因此,两个线程将"递增"属性,但只会增加1。

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


尝试一下:

1
2
3
4
5
6
7
8
9
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];
    });
}