atomic

atomic 在 setter 方法里加了锁,防止了多线程一直去写这个 property,造成难以预计的数值。

当属性使用 atomic 修饰时,它的读和写是原子性的:当线程 A 进行写操作,这时其他线程的读或者写操作会因为该操作而等待。当 A 线程的写操作结束后,B 线程进行写操作,然后当 A 线程需要读操作时,获得了在 B 线程中修改的值。如果有 C 线程在 A 线程读操作之前 release 了该属性,可能导致程序崩溃。

导致崩溃并不是线程安全问题。所谓线程安全是保证同一时间只有一个线程对该内存进行访问。只要我们使用 getter、setter 方法来访问,上面的表述中的每一个步骤都只有一条线程在访问该内存,哪个线程会获得锁完全取决于代码顺序,这个崩溃就是程序员自身的问题了。如果绕开 getter、setter 方法访问这个属性,才会造成线程不安全,比如使用 KVC。

一、atomic 是绝对安全的

在 64 位的操作系统下,所有类型的指针(包括 void *)都是占用 8 个字节的。超过 4 个字节的基本类型数据都会有线程并发的问题。

那所有的指针类型都会有这个问题。

以 Objective-C 的 NSArray * 为例子,如果一个多线程操作这个数据,会有两个层级的并发问题:

  1. 指针本身
  2. 指针所指向的内存

指针本身也是占用内存的,并且一定是 8 个字节(64 位系统)。第二部分,指针所指向的内存,有可能非常大,有可能也就 1 个字节。

所以考虑 NSArray * array 这个数据在进行多线程操作的时候,必须分成两部分来描述,一个是 &array 这个指针本身,另一个则是它所指向的内存 array。想象现在有两块内存,一块是 8 字节,一块 n 字节,8 字节里面放的值,就是 n 字节内存的首地址。

如果用 atomic 修饰之后,会有什么影响?

从内存的角度来解释这个过程。atomic 其实修饰的是这个指针 &array,与指针指向的第二部分 n 字节数据没有任何关系,被 atomic 修饰之后,你不可能随意去多线程操作这个 8 字节,但是对 8 字节里面所指向的 n 字节没有任何限制!

atomic 已经完美的履行了它的指责,你不可能对这个 8 字节进行无序的多线程操作,这就够了呀!有问题的是程序员,程序员并未对 n 字节做任何的限制。

二、NSMutableArray 本身是线程不安全的

简单来说,线程安全就是多个线程访问同一段代码,程序不会异常、不 Crash。而编写线程安全的代码主要依靠线程同步。

  1. 不使用 atomic 修饰属性。原因有二:

    • atomic 的内存管理语义是原子性的,仅保证了属性的 setter 和 getter 方法是原子性的、线程安全的,但是属性的其他方法,如数组添加/移除元素等并不是原子操作,所以不能保证属性是线程安全的。

    • atomic 虽然保证了 getter、setter 方法线程安全,但是付出的代价很大,执行效率要比 nonatomic 慢很多倍(有说法是慢 10-20 倍)。

    总之:使用 nonatomic 修饰 NSMutableArray 对象就可以了,而使用锁、dispatch_queue 来保证 NSMutableArray 对象的线程安全。

  2. 打造线程安全的 NSMutableArray

    《Effective Objective-C 2.0》书中第 41 条:多用派发队列,少用同步锁中指出:使用“串行同步队列”(serial synchronization queue),将读取操作及写入操作都安排在同一个队列里,即可保证数据同步。而通过并发队列,结合GCD 的栅栏块(barrier)来不仅实现数据同步线程安全,还比串行同步队列方式更高效。

    GCD 的栅栏块作用示意图

    说明:栅栏块单独执行,不能与其他块并行。直到当前所有并发块都执行完毕,才会单独执行这个栅栏块

    线程安全实现如下:

    @interface QSThreadSafeMutableArray()
    @property (nonatomic, strong) NSMutableArray * MDataArray;
    @property (nonatomic, strong) dispatch_queue_t MSyncQueue;
    @end
    
    @implementation QSThreadSafeMutableArray
    - (instancetype)initCommon
    {
        if (self = [super init]) {
            // %p 以 16 进制的形式输出内存地址,附加前缀 0x
            NSString * uuid = [NSString stringWithFormat:@"com.jzp.array_%p", self];
            // 注意:_MSyncQueue 是并行队列
            _MSyncQueue = dispatch_queue_create([uuid UTF8String], DISPATCH_QUEUE_CONCURRENT);
        }
        return self;
    }
    
    - (instancetype)init
    {
        if (self = [self initCommon]) {
            _MDataArray = [NSMutableArray array];
        }
        return self;
    }
    
    - (id)objectAtIndex:(NSUInteger)index
    {
        __block id obj;
        
        dispatch_sync(_MSyncQueue, ^{
            if (index < [_MDataArray count]) {
                obj = _MDataArray[index];
            }
        });
        return obj;
    }
    
    - (NSEnumerator *)objectEnumerator
    {
        __block NSEnumerator * enu;
        
        dispatch_sync( _MSyncQueue, ^{
            enu = [_MDataArray objectEnumerator];
        });
        return enu;
    }
    
    - (void)insertObject:(id)anObject atIndex:(NSUInteger)index
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if (anObject && index < [_MDataArray count]) {
                [_MDataArray insertObject:anObject atIndex:index];
            }
        });
    }
    
    - (void)addObject:(id)anObject
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if(anObject){
                [_MDataArray addObject:anObject];
            }
        });
    }
    
    - (void)removeObjectAtIndex:(NSUInteger)index
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if (index < [_MDataArray count]) {
                [_MDataArray removeObjectAtIndex:index];
            }
        });
    }
    
    - (void)removeLastObject
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            [_MDataArray removeLastObject];
        });
    }
    
    - (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject
    {
        dispatch_barrier_async( _MSyncQueue, ^{
            if (anObject && index < [_MDataArray count]) {
                [_MDataArray replaceObjectAtIndex:index withObject:anObject];
            }
        });
    }
    
    - (NSUInteger)indexOfObject:(id)anObject
    {
        __block NSUInteger index = NSNotFound;
        
        dispatch_sync( _MSyncQueue, ^{
            for (int i = 0; i < [_MDataArray count]; i ++) {
                if ([_MDataArray objectAtIndex:i] == anObject) {
                    index = i;
                    break;
                }
            }
        });
        return index;
    }
    
    - (void)dealloc
    {
        if (_MSyncQueue) {
            _MSyncQueue = NULL;
        }
    }
    
    @end
    

    说明 ①:使用 dispatch queue 实现线程同步;将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,又不会阻塞执行异步派发的线程;使用同步队列及栅栏块,可以令同步行为更加高效。

    说明 ②:NSMutableDictionary 本身也是线程不安全的,实现线程安全的 NSMutableDictionary 原理同线程安全的NSMutableArray。(代码见 QSUseCollectionDemo)

  3. 线程安全的 NSMutableArray 使用

    - (void)testQsMutableArray
    {
        _MSafeArray = [[QSThreadSafeMutableArray alloc] init];
        
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        for (NSInteger i = 0; i < 10; i++) {
            dispatch_async(queue, ^{
                NSString * str = [NSString stringWithFormat:@"数组%d", (int)i+1];
                [_MSafeArray addObject:str];
            });
        }
        
        sleep(1);
        
        NSEnumerator * enu = [_MSafeArray objectEnumerator];
        
        for (NSObject * object in enu) {
            NSLog(@"value: %@", object);
        }
    }
    

三、atomic 与 nonatomic 的区别

在默认情况下,由编译器生成的属性的 set、get 方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不需要同步锁。

尽管没有指明 atomic 的特质(如果某属性不具备 nonatomic 特质,那它就是"原子的"(atomic)),仍然可以在属性特质中写明这一点,编译器是不会报错的。

一般 iOS 程序中,所有属性都声明为 nonatomic。这样做的原因是:

  1. 在 iOS 中使用同步锁的开销比较大, 会带来性能问题。

  2. 一般情况下并不要求属性必须是"原子的",因为这并不能保证线程安全。若要实现线程安全的操作,还需采用更为深层的锁的机制。

一个线程在连续多次读取某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic 也还是会读取到不同的属性值。

因此,iOS 程序一般都会使用 nonatomic 属性。但在 Mac OS X 程序时,使用 atomic 属性通常都不会有性能瓶颈。

nonatomic 的实现:

- (void)setImage:(UIImage *)image
{
    if (_image != image) {
        [_image release];
        _image = [image retain];
        ...
    }
}

- (UIImage *)image
{
    return _image;
}

atomic 的实现:

- (void)setImage:(UIImage *)image
{
    @synchronized(self) {
        // 锁
        if (_image != image) {
            [_image release];
            _image = [image retain];
            ...
        }
    }
}

- (UIImage *)image
{
    @synchronized(self) {
        return _image;
    }
}

@synchronized 的介绍:

The @synchronized directive is a convenient way to create mutex locks on the fly in Objective-C code. The @synchronized directive does what any other mutex lock would do—it prevents different threads from acquiring the same lock at the same time. In this case, however, you do not have to create the mutex or lock object directly. Instead, you simply use any Objective-C object as a lock token, as shown in the following example:

- (void)myMethod:(id)anObj
{

@synchronized(anObj) {

// Everything between the braces is protected by the @synchronized directive.

}

}

The object passed to the @synchronized directive is a unique identifier used to distinguish the protected block. If you execute the preceding method in two different threads, passing a different object for the anObj parameter on each thread, each would take its lock and continue processing without being blocked by the other. If you pass the same object in both cases, however, one of the threads would acquire the lock first and the other would block until the first thread completed the critical section.

As a precautionary measure, the @synchronized block implicitly adds an exception handler to the protected code. This handler automatically releases the mutex in the event that an exception is thrown. This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes.

For more information about the @synchronized directive, see The Objective-C Programming Language.

更准确的说应该是读写安全,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。

四、文章

清雨未尽时 & NSMutableArray使用中忽视的问题

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容