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

2019-12-13  本文已影响0人  小心韩国人

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


@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资源,优先级低的线程就无法释放.

@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);
}
@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 *)这两句:

刚才我们说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( 低级锁 ).低级锁的特点就是睡觉.
@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
@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;


@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,就可以实现线程同步

@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
@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的效果.
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,如果有的属性的确需要线程同步操作,完全可以哪里需要哪里加锁.

上一篇 下一篇

猜你喜欢

热点阅读