多线程二:线程同步,OC中的各种锁

在多线程一:GCD中我们详细了解了GCD,如果多个线程同时占用一块资源,很可能会发生数据错乱和数据安全问题.所以我们今天了解一下线程同步概念.

  • 1:OSSpinLock导入#import
    OSSpinLock叫做自旋锁,等待锁的线程会处于忙等 (busy-wait)状态,一直占用着CPU资源,相当于while(1)循环;用法如下:

@interface OSSPinTest ()

@property (nonatomic,assign)OSSpinLock moneyLock;


@end


@implementation OSSPinTest

- (instancetype)init{
    if (self = [super init]) {
        self.moneyLock = OS_SPINLOCK_INIT;
    }
    return self;
}

//存钱
- (void)saveMoney
 //尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
 //    OSSpinLockTry(&_moneyLock);
//加锁
    OSSpinLockLock(&_moneyLock);
    [super saveMoney];
解锁
    OSSpinLockUnlock(&_moneyLock);
}
//取钱
- (void)drawMoney{
//尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
//    OSSpinLockTry(&_moneyLock);
//加锁
    [super drawMoney];
//解锁
     OSSpinLockUnlock(&_moneyLock);
}

需要注意的是,这种锁在iOS10.0后已经被弃用了,因为这种锁可能会出现优先级反转的问题,如果优先级低的线程抢到了这把锁,给这把锁加锁后,优先级高的线程就会一直处于等待状态,会一直占用CPU资源,优先级低的线程就无法释放.

  • 2:os_unfair_lock导入#import
    os_unfair_lock用于取代不安全的OSSpinLock,从iOS10.0才开始支持.但是和等待OSSpinLock锁的线程会处于忙等状态不同的是,等待os_unfair_lock锁的线程处于休眠状态.并非忙等.
@interface OSUnfairLock ()

@property (nonatomic,assign)os_unfair_lock moneyLock;
@property (nonatomic,assign)os_unfair_lock ticketLock;

@end


@implementation OSUnfairLock


- (instancetype)init{
    if (self = [super init]) {
        self.moneyLock = OS_UNFAIR_LOCK_INIT;
        self.ticketLock = OS_UNFAIR_LOCK_INIT;
    }
    return self;
        
}


- (void)saveMoney{
    os_unfair_lock_lock(&_moneyLock);
    [super saveMoney];
    os_unfair_lock_unlock(&_moneyLock);
}


- (void)drawMoney{
    //尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
    //    os_unfair_lock_trylock(&_moneyLock);
    os_unfair_lock_lock(&_moneyLock);
    [super drawMoney];
    os_unfair_lock_unlock(&_moneyLock);
}


- (void)ticket{
     //尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
    //    os_unfair_lock_trylock(&_ticketLock);
    os_unfair_lock_lock(&_ticketLock);
    [super ticket];
     os_unfair_lock_unlock(&_ticketLock);
}
  • 3: Mutex:互斥锁,等待锁的线程会处于休眠状态.mutex 是跨平台的
@interface MutexLock ()

@property (nonatomic,assign)pthread_mutex_t moneyLock;
@property (nonatomic,assign)pthread_mutex_t ticketLock;

@end

@implementation MutexLock

- (instancetype)init{
    if (self = [super init]) {
        //创建锁
        [self __initMutex:&_moneyLock];
        [self __initMutex:&_ticketLock];
    }
    return self;
        
}
//创建锁的属性 attr
- (void)__initMutex:(pthread_mutex_t *)mux{
    //创建属性
    pthread_mutexattr_t muteattr;
    pthread_mutexattr_init(&muteattr);
    //设置属性
    pthread_mutexattr_settype(&muteattr, PTHREAD_MUTEX_DEFAULT);
    //属性值有以下几种
    //        #define PTHREAD_MUTEX_NORMAL        0  //default
    //        #define PTHREAD_MUTEX_ERRORCHECK    1 //检查错误
    //        #define PTHREAD_MUTEX_RECURSIVE        2 //递归锁
    //        #define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL
    
    //创建锁
    pthread_mutex_init(mux, &muteattr);
    //销毁属性
    pthread_mutexattr_destroy(&muteattr);
}


- (void)saveMoney{
    //尝试加锁
//    pthread_mutex_trylock(&_moneyLock);
    pthread_mutex_lock(&_moneyLock);
    [super saveMoney];
    pthread_mutex_unlock(&_moneyLock);
    
}

- (void)drawMoney{
    pthread_mutex_lock(&_moneyLock);
    [super drawMoney];
    pthread_mutex_unlock(&_moneyLock);
    
}

- (void)ticket{
    
    pthread_mutex_lock(&_moneyLock);
    [super ticket];
    pthread_mutex_unlock(&_moneyLock);
}

如果是递归调用需要加锁,可以把属性设置为pthread_mutexattr_settype(&muteattr, PTHREAD_MUTEX_RECURSIVE); 递归锁允许同一个线程对一把锁进行重复加锁.

递归锁的案例

互斥锁有一个更高级的功能:比如说现在有这么一个需求,一个取钱操作和一个存钱操作,取钱操作必须要等到卡里的余额大于0的时候才可以进行.这种时候就需要用到Mutex中的pthread_cond_t了,先看代码:

@interface MutexConditionLock ()

@property (nonatomic,assign)int money;//余额
@property (nonatomic,assign)pthread_mutex_t mutex;//互斥锁
@property (nonatomic,assign)pthread_mutexattr_t attr;//互斥锁属性
@property (nonatomic,assign)pthread_cond_t conditaion;//条件

@end

@implementation MutexConditionLock


- (instancetype)init{
    if (self == [super init]) {
        //创建锁所需的属性
        pthread_mutexattr_init(&_attr);
        pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_DEFAULT);
        //创建互斥锁
        pthread_mutex_init(&_mutex, &_attr);
        //创建条件所需的属性
        pthread_condattr_t condattr;
        pthread_condattr_init(&condattr);
        //创建等待条件,第二个参数可以直接传 NULL
        pthread_cond_init(&_conditaion, &condattr);
    }
    return self;
}

- (void)otherTest{
    [[[NSThread alloc]initWithTarget:self selector:@selector(__drawMoney) object:nil]start];
     [[[NSThread alloc]initWithTarget:self selector:@selector(__saveMoney) object:nil]start];
}

//取钱
- (void)__drawMoney{
    pthread_mutex_lock(&_mutex);
    //如果 余额 为 0,就把锁解开,并进入休眠状态等待,等待其他线程唤醒,一旦唤醒后再次加锁
    if (self.money == 0) {
        pthread_cond_wait(&_conditaion, &_mutex);
    }
    self.money -= 50;
    NSLog(@"取钱后余额 %d",self.money);
    pthread_mutex_unlock(&_mutex);
}

//存钱
- (void)__saveMoney{
    pthread_mutex_lock(&_mutex);
    sleep(2);
    self.money += 100;
    NSLog(@"存钱余额 %d",self.money);
    //通知唤醒正在休眠等待中的线程
    pthread_cond_signal(&_conditaion);
    //如果多个线程都在等待信号唤醒就需要用到广播了
//    pthread_cond_broadcast(&_conditaion);
    pthread_mutex_unlock(&_mutex);
}

- (void)dealloc{
    pthread_mutexattr_destroy(&_attr);
    pthread_cond_destroy(&_conditaion);
}

运行结果:
2019-12-12 18:08:54.470700+0800 各种lockTest[3159:1068307] 存钱余额 100
2019-12-12 18:08:54.471072+0800 各种lockTest[3159:1068306] 取钱后余额 50

关键代码就在pthread_cond_wait(pthread_cond_t , pthread_mutex_t )pthread_cond_signal(pthread_cond_t *)这两句:

  • pthread_cond_wait(pthread_cond_t , pthread_mutex_t )传入两个参数,第一个参数就是等待唤醒的条件;第二个参数是互斥锁.
    以实例代码为例,如果首选进入__drawMoney方法,然后对_mutex加锁,如果self.money == 0符合条件,执行pthread_cond_wait(&_conditaion, &_mutex);,
  • pthread_cond_signal(pthread_cond_t *)传入一个条件,发送信号唤醒正在等待这个条件的线程.

刚才我们说OSSpinLock线程会处于忙等状态,我们从汇编代码看看是不是这样:
si(step instruction):是让汇编代码一行一行执行.
s (step):是让OC 代码一行一行执行.
next I:也是一样一行往下走,只不过遇到函数调用的时候不会进入函数内部,而si会进入函数内部.所以要想查看函数内部实现就要用si.

OSSpinLock 汇编

OSSpinLock汇编语言看到,底部就是一个while循环,一直处于忙等状态.

再来看看Mutex互斥锁的底层汇编:

Mutex 汇编

os_unfair_lock的底层汇编:

os_unfair_lock 汇编

从汇编可以看到os_unfair_lockMutex一样都是让等待的线程进入休眠状态.
另外苹果官方也说os_unfair_lock是一个Low-level lock( 低级锁 ).低级锁的特点就是睡觉.

  • 4:NSLock:是对Mutex普通锁封装.只是用起来更加面向对象,更加方便,主要有4个方法:
    NSLocking协议下的lock()unlock()方法以及自身的tryLock()lockBeforeDate:方法.我们只说一下lockBeforeDate:方法因为其他3个方法和mutex功能一样.
    lockBeforeDate :(NSDate*)date:传入一个时间,表示在这个时间之前线程会一直等待,如果等到别的线程放开这把锁就对这把锁加锁,并返回yes;如果在规定的时间还是没有等到这把锁,就加锁失败,返回NO代码继续往下走.会阻塞线程.
  • 5:NSRecursiveLock是对Mutex递归锁的封装.API和NSLock一致.
  • 6:NSCodition是对mutexcond的封装.主要有以下API:
    - (void)wait;等待条件唤醒
    - (BOOL)waitUntilDate:(NSDate *)limit;传入一个时间,在这个时间之前线程一直休眠等待.时间到了之后自动唤醒.
    - (void)signal;信号
    - (void)broadcast;广播
@interface NSConditionTest ()

@property (nonatomic,strong)NSCondition *moneyLock;

@property (nonatomic,assign)int money;//余额


@end


@implementation NSConditionTest


- (instancetype)init{
    if (self = [super init]) {

        //初始化锁
        self.moneyLock = [[NSCondition alloc]init];
    }
    return self;
        
}

- (void)otherTest{
    [[[NSThread alloc]initWithTarget:self selector:@selector(__drawMoney) object:nil]start];
     [[[NSThread alloc]initWithTarget:self selector:@selector(__saveMoney) object:nil]start];
}

//取钱
- (void)__drawMoney{
    [self.moneyLock lock];
    //如果 余额 为 0,就把锁解开,并进入休眠状态等待,等待其他线程唤醒,一旦唤醒后再次加锁
    if (self.money == 0) {
        [self.moneyLock wait];
    }
    self.money -= 50;
    NSLog(@"取钱后余额 %d",self.money);
    [self.moneyLock unlock];
}

//存钱
- (void)__saveMoney{
    [self.moneyLock lock];
    sleep(2);
    self.money += 100;
    NSLog(@"存钱余额 %d",self.money);
    //通知唤醒正在休眠等待中的线程
    [self.moneyLock signal];
    //如果多个线程都在等待信号唤醒就需要用到广播了
//    pthread_cond_broadcast(&_conditaion);
    [self.moneyLock unlock];
}
@end

运行结果
2019-12-13 10:25:50.905652+0800 各种lockTest[3621:1439700] 存钱余额 100
2019-12-13 10:25:50.905983+0800 各种lockTest[3621:1439699] 取钱后余额 50
  • 7:NSConditionLock:是对NSCondition的进一步封装,可以设置具体的条件值.可以控制线程间的执行顺序.主要API如下:
    @property (readonly) NSInteger condition;条件值
    - (void)lockWhenCondition:(NSInteger)condition;一直等到符合条件后加锁
    - (BOOL)tryLock;尝试加锁,如果锁已被其他线程加锁立马返回NO;如果未被加锁就加锁后返回YES.
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;尝试解锁,如果锁被其他线程占用立马返回NO,否则返回YES.
    - (void)unlockWithCondition:(NSInteger)condition;释放锁,并设置条件值
    - (BOOL)lockBeforeDate:(NSDate *)limit;是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;一直等待符合条件值后加锁.
@interface NSConditionLockDemo ()

@property (nonatomic,strong)NSConditionLock *lock;

@end


@implementation NSConditionLockDemo


- (instancetype)init{
    if (self = [super init]) {
        //初始化锁,条件值设为1
        self.lock = [[NSConditionLock alloc]initWithCondition:1];
    }
    return self;
        
}

- (void)otherTest{
    [[[NSThread alloc]initWithTarget:self selector:@selector(__test1) object:nil]start];
     [[[NSThread alloc]initWithTarget:self selector:@selector(__test2) object:nil]start];
}

- (void)__test1{
    //如果条件值为1,就加锁
    [self.lock lockWhenCondition:1];
    NSLog(@"1");
    sleep(2);
    //解锁,并把条件值设为2
    [self.lock unlockWithCondition:2];
}

- (void)__test2{
    //如果条件值为2,就加锁
    [self.lock lockWhenCondition:2];
    NSLog(@"2");
    [self.lock unlock];
}

@end

运行结果
2019-12-13 21:00:55.954512+0800 各种lockTest[4907:2111963] 1
2019-12-13 21:00:57.960116+0800 各种lockTest[4907:2111964] 2

注意如果initWithCondition创建的时候条件值没有设置或设置的nil,condition默认是0;

  • 8:dispatch_semaphore:可以用来控制线程并发访问的最大数量.

@interface SemaphoreDemo ()

@property (nonatomic,strong)dispatch_semaphore_t semaphore;
@property (nonatomic,strong)dispatch_semaphore_t money_semaphore;
@property (nonatomic,strong)dispatch_semaphore_t ticket_semaphore;


@end


@implementation SemaphoreDemo


- (instancetype)init{
    if (self = [super init]) {
        //初始化锁,条件值设为1
        self.semaphore = dispatch_semaphore_create(5);
        self.money_semaphore = dispatch_semaphore_create(1);
        self.ticket_semaphore = dispatch_semaphore_create(1);
    }
    return self;
        
}


- (void)saveMoney{
    dispatch_semaphore_wait(self.money_semaphore, DISPATCH_TIME_FOREVER);
    [super saveMoney];
    dispatch_semaphore_signal(self.money_semaphore);
}

- (void)drawMoney{
    dispatch_semaphore_wait(self.money_semaphore, DISPATCH_TIME_FOREVER);
    [super drawMoney];
    dispatch_semaphore_signal(self.money_semaphore);

}

- (void)ticket{
    dispatch_semaphore_wait(self.ticket_semaphore, DISPATCH_TIME_FOREVER);
    [super ticket];
    dispatch_semaphore_signal(self.ticket_semaphore);

}


- (void)otherTest{

    for (int i = 0; i < 30; i ++) {
        [[[NSThread alloc]initWithTarget:self selector:@selector(__test1) object:nil]start];
    }
   
}

- (void)__test1{
    //信号量的值 -1 继续往下执行代码
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_NOW);
    sleep(2);
    NSLog(@"1111");
    //信号量的值 +1
    dispatch_semaphore_signal(_semaphore);
}
@end

如果设置semaphore的初始值为5,就代表线程并发访问的最大值是5.他的实现原理是:如果信号量的初始值 <= 0,当前线程就会进入休眠状态等待,直到信号量的值 > 0;如果信号量的值 > 0,就先减1,然后往下执行代码.dispatch_semaphore_signal 会让信号量的值加1.
所以如果,设置信号量的值为1,控制线程的最大并发数为1,就可以实现线程同步

  • 9:dispatch_queue使用GCD的串行队列实现线程同步.把需要控制的操作都放到一个串行队列中:
@interface SerialQueueDemo ()

@property (nonatomic,strong)dispatch_queue_t serialQueue_money;
@property (nonatomic,strong)dispatch_queue_t serialQueue_ticket;
@end

@implementation SerialQueueDemo

- (instancetype)init{
    if (self == [super init]) {
        self.serialQueue_money = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
        self.serialQueue_ticket = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}


- (void)saveMoney{
    dispatch_sync(self.serialQueue_money, ^{
        [super saveMoney];
    });
}

- (void)drawMoney{
    dispatch_sync(self.serialQueue_money, ^{
        [super drawMoney];
    });

}

- (void)ticket{
    dispatch_sync(self.serialQueue_ticket, ^{
        [super ticket];
    });

}

@end
  • 10:synchronized:是对mutex递归锁的封装.这是最简洁的方式,但是性能比较差,苹果不推荐使用.
@implementation SynchronizedDemo

- (void)saveMoney{
     //传入的对象要保证是同一个对象
    @synchronized (self) { // objc_sync_enter 相当于加锁
        [super saveMoney];
    }// objc_sync_exit 相当于解锁
}

- (void)drawMoney{
    @synchronized (self) {//加锁
        [super drawMoney];
    }//解锁
}


- (void)ticket{
    static NSObject *lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc]init];
    });
    @synchronized (lock) {
        [super ticket];
    }
}

@end

跟进synchronized的汇编代码,会发现两个重要的函数:

synchronized 底层

objc-sync.mm中找到这两个函数:

enter exit 函数

到目前为止我们已经讲了10中线程同步的方法,那么我们在项目中应该使用哪一种呢:
效率 仅供产考

使用小技巧:
如果有好几个方法都需要加不同的锁,我们可以这样写:

- (void)test{
    static dispatch_semaphore_t semaphore;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        semaphore = dispatch_semaphore_create(1);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    //..
    //加锁操作
    //...
    dispatch_semaphore_signal(semaphore);
    
}

也可以把加锁的代码写成宏定义,这样更方便:

- (void)test{
    DispatchSemaphoreBegin
    //..
    //加锁操作
    //...
    DispatchSemaphoreEnd
}

自旋锁和互斥锁的比较:
什么情况下使用自旋锁比较划算?

  1. 预计线程等待的时间很短.
    如果线程等待的时间很短,就没必要让线程休眠等待,因为休眠后再唤醒也会消耗资源,降低性能.
  2. 加锁的代码(临界区)经常被调用,但竞争情况很少发生.
    临界区:lockunlock之间的代码我们称之为临界区.
  3. CPU资源不紧张
    什么情况下使用互斥锁比较划算?
  4. 预计线程等待锁的时间比较长,比如说2,3s.
  5. 单核处理器.
  6. 临界区有IO操作.
    IO操作的时间一般比较长,需要更多的CPU资源,而自旋锁会一直占用CPU资源,我们应该把CPU资源让出来给IO操作,所以IO操作用互斥锁比较合适.
  7. 临界区代码比较复杂或者循环量大.
  8. 临界区的竞争非常激烈.

iOS中实现多读单写
比如说现在有这种需求:
1:同一时间只能有一个线程进行写入文件的操作.
2:同一时间允许多个线程进行读取文件的操作.
3:同一时间,不允许读,写操作同时进行.
上面的需求就是多读单写操作,iOS中有两种方式实现多读单写操作:

  1. pthread_rwlock:读写锁
@interface PthreadRWlLockDemo ()

@property (nonatomic,assign)pthread_rwlock_t rwLock;


@end

@implementation PthreadRWlLockDemo



- (instancetype)init{
    if (self == [super init]) {
        //初始化读写锁
        pthread_rwlock_init(&_rwLock, NULL);
    }
    return self;
}


- (void)otherTest{
    for (int i = 0; i < 10; i ++) {
        [[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
        [[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
    }
}

- (void)read{
    //读操作加锁
    pthread_rwlock_rdlock(&_rwLock);
    sleep(1);
    NSLog(@"read");
    //解锁
    pthread_rwlock_unlock(&_rwLock);
}


- (void)write{
    //写操作加锁
    pthread_rwlock_wrlock(&_rwLock);
    sleep(1);
    NSLog(@"write");
    解锁
    pthread_rwlock_unlock(&_rwLock);
}

@end
  1. dispatch_barrier_async:异步栅栏调用
@interface BarrierLockDemo ()

@property (nonatomic,strong)dispatch_queue_t queue;


@end

@implementation BarrierLockDemo



- (instancetype)init{
    if (self == [super init]) {
        //初始化读写锁
        self.queue = dispatch_queue_create("readWirteQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}


- (void)otherTest{
    for (int i = 0; i < 10; i ++) {
        [[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
        [[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
    }
}

- (void)read{
    dispatch_async(self.queue, ^{
        sleep(1);
        NSLog(@"read");
    });
}

- (void)write{
    dispatch_barrier_async(self.queue, ^{
        sleep(1);
        NSLog(@"write");
    });
}

@end

异步栅栏的原理是:把写入文件的任务放到队列的时候,会给这个线程建立一个栅栏,围栏,不允许其他的任务进来.如图:

栅栏

使用异步栅栏的时候需要注意:传入这个函数的队列( queue )必须是通过dispatch_queue_create创建的,如果传入的是一个串行或者全局并发队列,那异步栅栏函数的功能就相当于dispatch_asyn的效果.

  • atomic:最后说一下atomic关键字.
    atomic是线程安全的,如果使用这个关键字修饰属性,系统会在属性的setter,getter方法内部加上加锁解锁的代码,我们看一下源代码:
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;// 如果是 nonatomic 直接返回值
        
    // Atomic retain release world
    //如果是 atomic
    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);
}


set 方法底层实现
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) {//如果是 nonatomic
        oldValue = *slot; //*slot 属性的内存地址
        *slot = newValue;
    } else {// atomic
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();//加锁
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();//解锁
    }

    objc_release(oldValue);
}

虽然atomic是线程安全的,但是我们在项目中还是不会使用,因为我们会非常频繁的访问属性,如果属性用atomic修饰,那会极大的消耗性能.所以我们项目中一般都是用nonatomic,如果有的属性的确需要线程同步操作,完全可以哪里需要哪里加锁.

你可能感兴趣的:(多线程二:线程同步,OC中的各种锁)