iOS面试题集合

iOS 底层原理 - 锁分析

2020-11-02  本文已影响0人  yan0_0

了解锁的机制会有助于项目开发,从而避免项目中多个线程访问同一块资源引发数据混乱的问题。

一 概念 锁的归类

基本的锁就包括了三类 自旋锁 互斥锁 读写锁,其他的比如条件锁,递归锁,信号量都是上层的封装和实现。

自旋锁

自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释 放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。

互斥锁

互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源(比 如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区 而达成
这里属于互斥锁的有:

NSLock
pthread_mutex
@synchronized

条件锁

条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就
是锁住了。当资源被分配到了,条件锁打开,进程继续运行
NSCondition
NSConditionLock

递归锁

递归锁:就是同一个线程可以加锁N次而不会引发死锁
NSRecursiveLock
pthread_mutex(recursive)

信号量

信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是
semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实
现更加复杂的同步,而不单单是线程间互斥。
dispatch_semaphore

读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须
自旋在那里,直到写者释放该读写锁。一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正
是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果
线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写
模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁.

锁的性能数据

屏幕快照 2020-11-02 下午5.34.41.png

二 @synchronized

@synchronized 是一种递归互斥锁。
应用如下

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 20;
    [self lg_testSaleTicket];
}


- (void)lg_testSaleTicket{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 3; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket{
    // 枷锁 - 线程安全
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张",self.ticketCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
    }

}

下面我们来分析下它的底层实现,通过在@synchronized (self) 打断点,显示汇编,可以看到底层调用了objc_sync_enter 和 objc_sync_exit方法,打符号断点看到方法是在libobjc.dylib库里面,也就是objc的源码,我们打开源码继续分析。


屏幕快照 2020-11-02 下午5.43.07.png

搜索objc_sync_enter方法,具体实现如下:

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

这里可以发现如果对象不存在就会走方法objc_sync_nil(),这里底层没有实现,也就是什么事情都不做,所以有时候会出现加锁了但是没用,因为对象被释放了,这里需要特别注意。

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

这里我们看下SyncData的结构,父类里面有子类,是一个指针结点,发现是一个链表结构

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

递归锁与objc_sync_nil搭配的使用,可以防止死锁现象发生。

继续往下看方法id2data

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
    
    // @syn - vc model view 任何地方都可以直接使用 - 全局
    // data 存储节点
    // map - obj - 表
    
#if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object
    // 检查每线程单项快速缓存中是否有匹配的对象
    bool fastCacheOccupied = NO;
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
        fastCacheOccupied = YES;

        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                lockCount++;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            case RELEASE:
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    // 检查已拥有锁的每个线程高速缓存中是否有匹配的对象
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int I;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[I];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE:
                item->lockCount++;
                break;
            case RELEASE:
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

看第一句代码里LOCK_FOR_OBJ,是一个宏,sDataLists的真实类型是SyncList,所以我们猜测StripedMap应该有个全局的哈希表来通过obj得到一个index,然后存储SyncList,而SyncList存储的都是一个个的节点SyncData。

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data

static StripedMap<SyncList> sDataLists;
struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
屏幕快照 2020-11-02 下午2.48.54.png

总结:@synchronized使用简单,但是性能不高,因为底层是对哈希表进行增删改查,有漏洞。

@synchronized的注意点

我们先看一段代码

for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            _testArray = [NSMutableArray array];
        });
    }

这里不断创建线程创建访问对象,会造成可变数组的不安全性,运行代码,crash掉,如下所示:


屏幕快照 2020-11-02 下午3.14.18.png

创建的过程是产生一个new value,old value release掉的过程,会有一瞬间_testArray为nil,我们可以通过加锁解决,这里如果使用@synchronized的话,不会产生作用,因为当对象为空时,锁不起作用。

for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
            
        });
    }

这时候可以使用NSLock来解决。

三 NSLock

NSLock是「非」递归互斥锁,具体使用如下:

NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            // old value ->
            _testArray = [NSMutableArray array];
            [lock unlock];

        });
    }

点进去发现NSLock在Foundation库里面

屏幕快照 2020-11-02 下午3.27.00.png

NSLock 是一种互斥锁,也是非递归的互斥锁,所以在递归时,会造成线程的堵塞。

这里我们看一个例子,这里递归调用,使用互斥锁NSLock,运行发现结果只打印10,产生了堵塞,所以递归调用这里不能使用互斥锁,应该使用递归锁NSRecursiveLock

NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                [lock lock];
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [lock unlock];
                
            };
            testMethod(10);
        });

运行结果

2020-11-02 15:34:40.854430+0800 002-@synchronized注意点[3938:109845] current value = 10

四 NSRecursiveLock

修改上面例子,代码如下:

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
        static void (^testMethod)(int);
        
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [lock unlock];
            
        };
        testMethod(10);
    });

运行结果正常


屏幕快照 2020-11-02 下午3.39.33.png

如果我们给上面的代码加一个for循环,就会造成一个死锁现象

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

    for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                [lock lock];
                if (value > 0) {
                  NSLog(@"current value = %d",value);
                  testMethod(value - 1);
                }
                [lock unlock];
                
            };
            testMethod(10);
        });
    }
屏幕快照 2020-11-02 下午4.02.47.png

这里改为@synchronized就能解决死锁,因为 @synchronized 锁的是同一个对象,当下次再来的时候只会从 cache 里面取,而不会像 NSRecursiveLock 当线程 1 锁了一次之后,线程 2 来了还会再锁一次,所以造成了线程 1 等线程 2 解锁,线程 2 等线程 1 解锁的死锁状况。

for (int i= 0; i<100; i++) {
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
           
            static void (^testMethod)(int);
            
            testMethod = ^(int value){
                @synchronized (self) {
                    if (value > 0) {
                        NSLog(@"current value = %d",value);
                        testMethod(value - 1);
                    }
                }
               
                
            };
            testMethod(10);
        });
    }

总结

普通线程安全使用NSLock
递归调用线程安全使用NSRecursiveLock
产生循环,受外界线程影响的时候要特别注意死锁,就算损失一点点性能,也要保证安全

四 NSCondition

NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要 为了当检测条件时保护数据源,执行条件引发的任务;线程检查器 主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

1:[condition lock];//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
2:[condition unlock];//与lock 同时使用
3:[condition wait];//让当前线程处于等待状态
4:[condition signal];//CPU发信号告诉线程不用在等待,可以继续执行

NSCondition 使用场景更适用于生产消费者模式,当你生产大于 0 的时候,我可以进行消费,如果一直等于 0 的时候,我就一直等你生产,例子如下:

@interface ViewController ()
@property (nonatomic, assign) NSUInteger ticketCount;
@property (nonatomic, strong) NSCondition *testCondition;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 0;
    [self lg_testConditonLock];
}



#pragma mark -- NSCondition

- (void)lg_testConditon{
    
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        
    }
}

- (void)lg_producer{
    [_testCondition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal];
    [_testCondition unlock];

}

- (void)lg_consumer{
    
    // 线程安全
    [_testCondition lock];

    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保证正常流程
        [_testCondition wait];
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_testCondition unlock];
}



五 NSConditionLock

NSConditionLock 是对 NSCondition 的进一步封装,可以设置具体的条件值对象。NSConditionLock 可以确保只有在满足特定条件时,线程才能获得锁。
1.1 NSConditionLock 是锁,一旦一个线程获得锁,其他线程一定等待
1.2 [xxxx lock]; 表示 xxx 期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
1.3 [xxx lockWhenCondition:A条件]; 表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并 且没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码 的完成,直至它解锁。
1.4 [xxx unlockWithCondition:A条件]; 表示释放锁,同时把内部的condition设置为A条件
1.5 return = [xxx lockWhenCondition:A条件 beforeDate:A时间]; 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理
1.6 所谓的condition就是整数,内部通过整数比较条件

具体使用如下:

#pragma mark -- NSConditionLock
- (void)lg_testConditonLock{
    // 信号量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
       [conditionLock lockWhenCondition:1];
       NSLog(@"线程 1");
       [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
       [conditionLock lockWhenCondition:2];
       
       NSLog(@"线程 2");
       
       [conditionLock unlockWithCondition:1];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}

打印结果是321。

线程 1 调用[NSConditionLock lockWhenCondition:],此时此刻因为不满足当前条件,所
以会进入 waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。
此时当前的线程 3 调用[NSConditionLock lock:],本质上是调用 [NSConditionLock
lockBeforeDate:],这里不需要比对条件值,所以线程 3 会打印
接下来线程 2 执行[NSConditionLock lockWhenCondition:],因为满足条件值,所以线程
2 会打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候讲
value 设置为 1,并发送 boradcast, 此时线程 1 接收到当前的信号,唤醒执行并打印。
自此当前打印为 线程 3->线程 2 -> 线程 1。
[NSConditionLock lockWhenCondition:]:这里会根据传入的 condition 值和 Value 值进
行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行
[NSConditionLock unlockWithCondition:]: 这里会先更改当前的 value 值,然后进行广
播,唤醒当前的线程。

六 atomic

atomic原理

所有为属性赋值的方法在底层调用的是reallySetProperty方法,在源码里全局搜索,可以看到原子性和非原子性调用方法的区别

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}

主要区别是第五个参数

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
          //自旋锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

取值的时候在底层会调用objc_getProperty方法,全局搜索:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

所以atomic的本质是对setter和getter(reallySetProperty,objc_getProperty)方法分别加了一把锁,生成了原子性的get,set方法。

注意点

atomic修饰属性加的锁,仅仅是在setter和getter内部加的锁,保证取值/赋值时的线程安全性,但是如果像下面这样使用并不能保证线程安全。
例子一

@property (atomic, strong) NSArray *array;

- (void)lg_test_atomic2{
    //Thread A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100000; i ++) {
            if (i % 2 == 0) {
                self.array = @[@"Hank", @"CC", @"Cooci"];
            }
            else {
                self.array = @[@"Kody"];
            }
            NSLog(@"Thread A: %@\n", self.array);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 100000; i ++) {
            if (self.array.count >= 2) {
                NSString* str = [self.array objectAtIndex:1];
            }
            NSLog(@"Thread B: %@\n",self.array);
        }
    });
}

运行代码崩溃

看第二个例子:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
            NSLog(@"Thread A: %@--%ld", NSThread.currentThread,(long)self.num);
        }
    });
    
    //Thread B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i = 0; i < 10000; i ++) {
            self.num = self.num + 1;
            NSLog(@"Thread A: %@--%ld", NSThread.currentThread,(long)self.num);
        }
    });

理想结果应该是20000,运行代码,结果为19833,证明在某个时刻两个线程访问了一个数据,重复赋值,self.num的setter方法和getter方法是线程安全的,但是self.num + 1这句代码不是线程安全的,不受保护。
总而言之,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。
平时不建议使用atomic,会消耗更多的资源,性能会很低,要比nonatomic慢20倍。

七 自定义读写锁

我们可以创建一个并发队列,对于读操作,我们可以用同步函数进行处理,对于写操作我们可以用异步栅栏函数这操作,这样就简单实现了一个读写锁

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGRWLock : NSObject
// 读数据
- (id)lg_objectForKey:(NSString *)key;
// 写数据
- (void)lg_setObject:(id)obj forKey:(NSString *)key;
@end

@interface LGRWLock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end

@implementation LGRWLock

- (id)init{
    self = [super init];
    if (self){
        // 创建一个并发队列:
        self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据字典:
        self.dataCenterDic = [NSMutableDictionary dictionary];
    }
    return self;
}

#pragma mark - 读数据
- (id)lg_objectForKey:(NSString *)key{
    __block id obj;
    // 同步读取指定数据:
    dispatch_sync(self.concurrent_queue, ^{
        obj = [self.dataCenterDic objectForKey:key];
    });
    return obj;
}

#pragma mark - 写数据
- (void)lg_setObject:(id)obj forKey:(NSString *)key{
    // 异步栅栏调用设置数据:
    dispatch_barrier_async(self.concurrent_queue, ^{
        [self.dataCenterDic setObject:obj forKey:key];
    });
}

@end
上一篇下一篇

猜你喜欢

热点阅读