iOS底层探索-锁

422 阅读5分钟

1、atomic、nonatomic

1.1、set方法

void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    // 调用 reallySetProperty 方法
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
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);

    // 若设置为 copy 属性
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        // nonatomic 直接赋值
        oldValue = *slot;
        *slot = newValue;
    } else {
        // atomic 赋值操作需要加锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}
  • 如果是atomic属性,进行set前会进行加锁,set后解锁

1.2、get方法

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属性,进行get前会进行加锁

小结

  • atomic 是对属性的 setter 与 getter 进行加锁,单次读写能保证完整,但多线程下但并不能完全保证线程安全

  • 单线程 中使用atomic执行 setter 和 getter 时线程安全(如setter过程中调用了getter,那么会等到setter完成后才会执行getter),但在 多线程 下又有getter又有setter时,get到的值不可控(线程1调用 getter,线程2调用 setter,线程3调用 setter 这3个线程并行同时开始image.png

2、锁的类型

2.1、自旋锁

  • 是一种盲等线程不会休眠,相当于有do while循环卡在里边
  • 因为线程不会进行休眠,省去了线程间的调度,因此 执行效率高,但 占用大量资源
2.1.1、OSSpinLock(苹果已废弃)
  • 在线程有不同优先级的情况下会出现 优先级反转 的问题
2.1.2、atomic
2.1.3、dispatch_semaphore_t

2.2、互斥锁

  • 是一种闲等,资源被锁时线程会休眠,CPU会去调度其他线程工作
2.2.1、os_unfair_lock

image.png

方法作用
OS_UNFAIR_LOCK_INIT初始化锁
os_unfair_lock_lock(&锁)加锁
os_unfair_lock_unlock(&锁)解锁
os_unfair_lock_trylock(&锁)尝试加锁,成功返回true、失败返回false
因为会触发崩溃因此不常用的:
os_unfair_lock_assert_owner(&锁)若当前线程未持有指定锁或者已解锁,则崩溃
os_unfair_lock_assert_not_owner(&锁)若当前线程持有指定锁则崩溃
  • 如果在使用的位置直接声明的锁,还是会有线程安全的情况 image.png
2.2.2、NSLock
  • 不能递归调用(未对加锁资源解锁时又进行加锁),否则死锁 image.png
方法作用
-(void)lock加锁
-(void)unlock解锁
-(BOOL)trylock尝试加锁,成功返回YES、失败返回NO
-(BOOL)lockBeforeDate:(NSDate *)limit在指定时间点之前获取锁,能获取返回YES、不能返回NO
@property(nullable,copy)NSString *name锁名称
2.2.3、NSRecursiveLock(递归锁)
  • NSLock 方法相同,区别在于可用于递归中 image.png
  • 必须在同一线程下,比如将上例的 dispatch_async 置于for循环中就会产生死锁,因为会开辟多条线程
2.2.4、NSCondition

image.png

  • 虚假唤醒:如果有多个线程在条件变量发出信号时等待他,系统可能会将所有线程唤醒(signal当做broadcast),从而打破了信号与唤醒间预期的1:1关系
方法作用
-(void)lock加锁
-(void)unlock解锁
-(void)wait阻塞当前线程,使线程进入休眠,等待唤醒信号,调用前必须已加锁
-(void)waitUntilDate阻塞当前线程,使线程进入休眠,等待唤醒信号或超时,调用前必须已加锁
-(void)signal唤醒一个正在休眠的线程,若要唤醒多个则需调用多次,调用前必须已加锁
-(void)broadcast唤醒所有在等待的线程,调用前必须已加锁
@property(nullable,copy)NSString *name锁名称
2.2.5、NSConditionLock
  • 是基于 NSCondition 的封装,自带条件探测,可以实现控制线程调用顺序的需求,与信号量相似 image.png

2.3、读写锁

多读单写、读与写互斥

// 通过GCD实现读写锁
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic,strong) dispatch_queue_t queue;
@property (nonatomic,strong) NSMutableDictionary *dic;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.queue = dispatch_queue_create("LZ", DISPATCH_QUEUE_CONCURRENT);
    self.dic = [NSMutableDictionary new];

    // 多读通过for循环开辟线程实现
    for (int i = 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [self readMethod];
        });
    }
}

- (NSString *)readMethod {
    __block NSString *ret;

    // 与 写操作 在同一队列中;使用sync是为了保证return不会先被执行
    dispatch_sync(self.queue, ^{
        ret = self.dic[@"name"];
    });
    NSLog(@"%@",ret);
    return ret;
}

- (void)writeMethod:(NSDictionary *)dic {
    // 通过栅栏函数,和 读操作 在同一队列中,来保证读写互斥
    dispatch_barrier_async(self.queue, ^{
        [self.dic setDictionary:dic];
    });
}
  • 多读单写:async开辟多线程进行读操作
  • 读写互斥:通过将 读操作 和 写操作 放在在同一队列中,且 写操作 使用栅栏函数,来实现读写互斥

小结

  • 自旋锁是盲等互斥锁是闲等

  • @synchronized 在模拟器下性能不及其他锁,但在真机下与其他锁效率相差不大,又因它的使用简单,因此被使用的最广泛,也单独作为一篇

  • 遵守NSLocking协议的都有 lockunlock 方法

    @protocol NSLocking
    
    - (void)lock;
    - (void)unlock;
    
    @end
    
  • 普通锁与递归锁的区别:

    • 锁:同一时刻只能被一条线程锁拥有
    • 递归锁:同一时刻能被多条线程锁拥有
  • 很多锁其实都是基于pthread_mutex的封装

    • 构造方法init()就是调用了pthread的pthread_mutex_init(mutex, nil)方法
    • 析构方法deinit就是调用了pthread的pthread_mutex_destroy(mutex)方法
    • 加锁方法 lock()就是调用了pthread的pthread_mutex_lock(mutex)方法
    • 解锁方法 unlock()就是调用了pthread的pthread_mutex_unlock(mutex)方法
    • pthread_mutex中可以通过pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))来设置锁为递归锁,像NSLock就并没有设置,所以在使用的时候需要注意 NSLock 并不能在递归函数中使用

3、pthread_mutex

既然很多锁都是基于pthread的封装,我们下边列出pthread的方法作为资料以供参考:

方法作用
pthread_mutex_init(pthread_mutex_t mutex, const pthread_mutexattr_t attr)初始化锁,pthread_mutexattr_t可用来设置锁的类型(是否为递归锁)
pthread_mutex_lock(pthread_mutex_t mutex)加锁
pthread_mutex_trylock(*pthread_mutex_t *mutex)加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息
pthread_mutex_unlock(pthread_mutex_t *mutex)解锁
pthread_mutex_destroy(pthread_mutex_t* mutex)使用完锁之后释放锁
pthread_mutexattr_setpshared()设置互斥锁的范围
pthread_mutexattr_getpshared()获取互斥锁的范围