OC 多线程基础知识:锁

OC 多线程基础知识:锁

想要深入理解多线程,锁是预备知识,这里总结一下OC中锁相关的知识,打好基础。

为什么要有锁?

锁概念的提出,是为了解决多线程资源共享的问题,在多线程环境下,有的资源可能会同时被多个线程访问,可能会出现资源抢夺的问题。这里引入一个概念叫临界区(Critical Section),就是一段代码,同一时间只能由一个线程访问,以保障临界区内的线程是安全的(资源不被抢夺,改变)。锁就是用来解决临界区内线程安全的问题。

下面介绍三种常见的锁:

自旋锁(spin lock)

自旋锁长这样:

while (抢锁(lock) == 没抢到) {
}

就是利用一个while循环,不断的尝试去抢锁(这里的锁lock是一个抽象的概念,可以是一个整数,一开始是1,表示没有锁,抢到后变为0,表示有锁),抢到了锁就跳出循环,抢不到锁就不断重试。

自旋锁的缺点很明显,不断的抢锁会占用CPU资源。优点是线程不用休眠,不用花时间在上下文切换(context switch)从用户态转化为内核态,用在轻量级的临界区上效率高。

互斥锁(mutex

互斥锁长这样:

while (抢锁(lock) == 没抢到) {
    线程休眠,请在这把锁的状态发生改变时再唤醒(lock);
}

和自旋锁很相似,不同就是抢不到锁的时候,让线程去休眠,当锁的状态改变的时候再唤醒该线程。

互斥锁的缺点是,线程休眠会让线程从用户态转化为内核态,唤醒的时候从内核态转化为用户态,需要两次上下文切换,花费大量时间。优点是不用忙等,临界区很长时效率高。

读写锁(readers-writer lock

读写锁就是分了两种情况,一种是读时的锁,一种是写时的锁,同时规定:

  • 同时可以存在多个读锁,也就是读-读不互斥
  • 只能存在一个写锁,也就是读-写互斥,写-写互斥

读写锁的实现时用了两个互斥锁(或者两个自旋锁):

//读者加锁
- (void)readerLock {
    加锁(rlock);
    condition++;
    if (condition == 1) {
        加锁(wlock);
    }
    解锁(rlock);
}
//读者解锁
- (void)readerUnlock {
    加锁(rlock);
    condition--;
    if (condition == 0) {
        加锁(wlock);
    }
    解锁(rlock);
}
//写者加锁
- (void)writerLock {
    加锁(wlock);
}
//写者解锁
- (void)writerUnlock {
    解锁(wlock);
}
@end

这里我们用了两把互斥锁(rlockwlock)来实现读写锁,利用了:

  • 计数器condition跟踪被阻塞的读线程。如果先有写锁,读锁中condition==1,读会被wlock阻塞。如果先有读锁,写锁会被wlock堵塞;读锁再次获取时,可以使condition>1,从而读锁不被堵塞。
  • 互斥锁rlock保护condition,供读者使用
  • 互斥锁wlock 确保写操作互斥

下面介绍一个更高级的实现读写锁的方法:条件变量+互斥锁

首先介绍一下条件变量

条件变量可以简单理解为,一个条件,如果达成了就发通知。这样说有点抽象,把条件变量用到读写锁里就清楚了:

//读者加锁
- (void)readerLock {
    加锁(rwlock);
    while (self.isWriting) {
        解锁,等待条件变量达成时的通知唤醒,再加锁(cond, rwlock);
    }
    self.readCount++;
    解锁(rwlock);
}
//读者解锁
- (void)readerUnlock {
   加锁(rwlock);
   self.readCount--;
   if (self.readCount == 0) {
       //唤起一条写的线程
       条件变量达成时,触发通知(cond);
   }
   解锁(rwlock);
}
//写者加锁
- (void)writerLock {
    加锁(rwlock);
    while (self.isWriting || self.readCount > 0) {
         解锁,等待条件变量达成时的通知唤醒,再加锁(cond, rwlock);
    }
    self.isWriting = YES;
    解锁(rwlock);
}
//写者解锁
- (void)writerUnlock {
    加锁(rwlock);
    self.isWriting = NO;
    //唤起多个读的线程
    条件变量达成时的,触发通知(cond);
    解锁(rwlock);
}
@end

这里使用了使用[条件变量cond与普通的互斥锁rwlock、整型计数器readCount(表示正在读的个数)与布尔标志isWrite(表示正在写)来实现读写锁。

  • 当有读锁时,readCount>0,写锁进入while循环,只有条件变量达成时,才会收到通知唤醒跳出循环,条件变量达成的条件就是读锁全部释放(readCount==0)。
  • 当有写锁时,isWriting == YES,读锁进入while循环,只有条件变量达成时,才会收到通知唤醒出循环,条件变量达成的条件就是写锁释放(isWriting == NO)。

下面总结一个OC钟常用锁的用法,一下所有例子,都用卖股票的例子:

@synchronized 关键字

@synchronized(这里添加一个OC对象,一般使用self) {
	要加锁的代码
}

这是一个互斥锁,简单易用,但性能最差,建议加锁的代码尽量少,例子如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        @synchronized(self) {
            if (self.tickets > 0) {
                self.tickets -= 1;
                NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
                break;
            }
        }
    }
}

// 剩余票数=4, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}
// 票卖完了  Thread:<NSThread: 0x60000249cd80>{number = 5, name = (null)}
// 票卖完了  Thread:<NSThread: 0x6000024c0040>{number = 6, name = (null)}

NSLock

_mutexLock = [[NSLock alloc] init];
[_mutexLock lock];
[_mutexLock unlock];

互斥锁,有lockunlock方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);

    _mutexLock = [[NSLock alloc] init];

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        [_mutexLock lock];
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        [_mutexLock unlock];
    }
}

// 剩余票数=4, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600003758900>{number = 6, name = (null)}
// 票卖完了  Thread:<NSThread: 0x6000037423c0>{number = 3, name = (null)}

pthread_mutex

pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);

互斥锁

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    // @property pthread_mutex_t mutex;
    pthread_mutex_init(&_mutex, NULL);

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        pthread_mutex_lock(&_mutex);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        pthread_mutex_unlock(&_mutex);
    }
}

@end
  
// 剩余票数=4, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600003e36a00>{number = 6, name = (null)}
// 票卖完了  Thread:<NSThread: 0x600003e49840>{number = 7, name = (null)}

dispatch_semaphore

_semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(_semaphore);

信号量实现加锁,线程获取一个信号量时,信号量数量减一,线程释放信号量时,信号量数量加一,信号量数量大于等于零时,加锁的代码可以执行。互斥锁可以看做是一种特殊的信号量(初始信号量等于一)。

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    _semaphore = dispatch_semaphore_create(1);

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        dispatch_semaphore_signal(_semaphore);
    }
}

@end
  
// 剩余票数=4, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x60000184f600>{number = 4, name = (null)}
// 票卖完了  Thread:<NSThread: 0x600001815e40>{number = 7, name = (null)}

OSSpinLock

_pinLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&_pinLock);
OSSpinLockUnlock(&_pinLock);

自旋锁,效率最高,但有隐患:

可能会出现优先级翻转的情况。比如线程1优先级比较高,线程2优先级比较低,然后在某一时刻是线程2先获取到锁,所以先是线程2加锁,这时候,线程1就在while(目标锁还未释放),这个状态,但因为线程1优先级比较高,所以系统分配的时间比较多,有可能会没有分配时间给线程2执行后续的操作(需要做的任务和解锁)了,这时候就会造成死锁。

所以iOS10之后自旋锁OSSpinLock就被os_unfair_lock(底层是互斥锁)替换了。

- (void)viewDidLoad {
    [super viewDidLoad];

    //设置票的数量为5
    self.tickets = 5;

    self.q1 = dispatch_queue_create("queue1", DISPATCH_QUEUE_CONCURRENT);
    
    // #import <libkern/OSAtomic.h>
    // @property OSSpinLock pinLock;
    // 'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
    _pinLock = OS_SPINLOCK_INIT;

    //线程1
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });

    //线程2
    dispatch_async(self.q1, ^{
        [self saleTickets];
    });
}

- (void)saleTickets {
    while (1) {
        [NSThread sleepForTimeInterval:1];
        OSSpinLockLock(&_pinLock);
        if (self.tickets > 0) {
            self.tickets -= 1;
            NSLog(@"剩余票数=%d, Thread:%@", self.tickets, [NSThread currentThread]);
        } else {
            NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
            break;
        }
        OSSpinLockUnlock(&_pinLock);
    }
}

@end
  
// 剩余票数=4, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票数=3, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票数=2, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 剩余票数=1, Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
// 剩余票数=0, Thread:<NSThread: 0x600000a88cc0>{number = 4, name = (null)}
// 票卖完了  Thread:<NSThread: 0x600000afcb00>{number = 3, name = (null)}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值