iOS学习-锁
在 iOS 中,锁是多线程编程中的关键要素,也是保证线程安全的重要手段之一。本文将介绍常见的几种锁及其使用场景,并提供相关的代码示例。
iOS中的锁主要可以分为两大类,互斥锁和自旋锁,其他锁都是这两种锁的延伸和扩展。
一、自旋锁
原理
自旋锁是一种基于忙等待的锁,当多个线程访问共享资源时,自旋锁会不停地进行循环检查,直到获取到锁为止。自旋锁的好处在于它避免了线程切换和上下文切换的开销,在多核 CPU 上,自旋锁可以充分利用 CPU 时间片,因此在锁竞争不激烈的情况下,自旋锁的性能比互斥锁好。
使用场景
自旋锁适用于以下场景:
- 多个线程访问共享资源的竞争不激烈,即锁竞争不激烈。
- 访问共享资源的时间很短,即不会发生线程挂起。
iOS只有一种自旋锁:OSSpinLock,其使用方法如下:
#import <libkern/OSAtomic.h>
OSSpinLock spinLock = OS_SPINLOCK_INIT;
// 获取锁
OSSpinLockLock(&spinLock);
// 临界区代码
// 释放锁
OSSpinLockUnlock(&spinLock);
需要注意的是,由于自旋锁等待线程不会进入阻塞状态,而是不断尝试获取锁,如果等待时间过长会导致CPU占用过高,从而影响应用程序的性能。此外,OSSpinLock还存在优先级反转的问题。
优先级反转指的是高优先级的线程因等待低优先级线程所持有的锁而被阻塞的情况。在这种情况下,高优先级的线程可能一直等待,直到低优先级的线程释放锁。并且高优先级的线程会抢占CPU资源,低优先级的线程得不到CPU调度,进而延长了等待时间,导致高优先级的任务被延迟执行。
由于这些原因,OSSpinLock已被苹果废弃。
二、互斥锁
互斥锁是一种常见的锁,用于保护临界区代码,防止多个线程同时访问共享资源。
与自旋锁不同的是,互斥锁会将未获得锁的线程挂起,等待锁的释放。这种操作需要进行上下文切换,开销较大,因此互斥锁的性能不如自旋锁。
iOS 中的常见互斥锁包括 pthread_mutex_t、NSLock、NSCondition、NSConditionLock、NSRecursiveLock等。
1.pthread_mutex_t
pthread_mutex是一种互斥锁,它可以保证同一时间只有一个线程可以访问共享资源。pthread_mutex基于POSIX标准实现。
pthread_mutex与其他锁机制相比,具有以下特点:
- 轻量级,适合高并发场景。
- 由于是C语言的库,使用起来相对较复杂。
代码示例
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 获取锁
pthread_mutex_lock(&mutex);
// 临界区代码
// 释放锁
pthread_mutex_unlock(&mutex);
2.NSLock
NSLock是一个简单的互斥锁,它基于POSIX线程互斥锁实现。
代码示例
NSLock *lock = [[NSLock alloc] init];
// 获取锁
[lock lock];
// 临界区代码
// 释放锁
[lock unlock];
3. NSCondition
NSCondition是一种条件锁,它可以让线程在某个条件满足时等待,并在条件满足时唤醒线程。NSCondition基于pthread_cond_t和pthread_mutex_t实现。
NSCondition适用于以下场景:
- 需要等待某个条件满足后再进行访问的场景。
- 支持多个条件等待和唤醒的场景。
代码示例
// 初始化条件锁和条件变量
NSCondition *condition = [[NSCondition alloc] init];
BOOL conditionMet = NO;
- (void)waitExample {
// 等待条件满足
[condition lock];
while (!conditionMet) {
[condition wait];
}
// 访问共享资源
// 释放锁
[condition unlock];
}
- (void)signalExample {
// 唤醒等待的线程
[condition lock];
conditionMet = YES;
[condition signal];
[condition unlock];
}
4. NSConditionLock
NSConditionLock是一种条件锁,它可以让线程在不同的条件下等待。NSConditionLock基于NSCondition和一个内部的状态变量实现。
NSConditionLock与其他锁机制相比,具有以下特点:
- 支持多个条件等待和唤醒。
- 支持在不同的条件下等待。
代码示例
// 初始化条件锁
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:0];
- (void)conditionLockExample {
// 等待条件1
[conditionLock lockWhenCondition:0];
// 访问共享资源
// 设置条件2
[conditionLock unlockWithCondition:1];
// 等待条件2
[conditionLock lockWhenCondition:1];
// 访问共享资源
// 释放锁
[conditionLock unlock];
}
5. NSRecursiveLock
NSRecursiveLock是一种递归锁,它允许同一个线程多次获取锁,避免死锁。
NSRecursiveLock与其他锁机制相比,具有以下特点:
- 支持递归锁,同一线程可以多次获取同一把锁,避免死锁。
- NSRecursiveLock的性能比NSLock略低,因为它需要维护额外的状态以支持递归锁。
代码示例
// 初始化递归锁
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
- (void)recursiveLockExample {
// 获取递归锁
[recursiveLock lock];
// 访问共享资源
// 再次获取递归锁
[recursiveLock lock];
// 访问共享资源
// 释放递归锁
[recursiveLock unlock];
// 释放递归锁
[recursiveLock unlock];
}
三. 信号量
原理
信号量是一种常见的并发控制机制,用于控制对共享资源的访问。当多个线程访问共享资源时,信号量可以用来限制并发访问的数量。信号量有一个计数器,每个线程访问共享资源前需要获取信号量,如果计数器为0,则线程需要等待,直到其他线程释放信号量为止。如果计数器不为0,则线程可以访问共享资源,并将计数器减1。当线程访问完共享资源后,需要释放信号量,使计数器加1,以便其他线程可以访问共享资源。
区别
与互斥锁和自旋锁不同的是,信号量可以控制对共享资源的并发访问数量,因此它更适合用于限制并发度较高的情况。
使用场景
- 多个线程访问共享资源的竞争激烈,即锁竞争激烈。
- 需要控制并发访问的数量。
代码示例
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
- (void)lockExample {
// 等待信号量
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 访问共享资源
// 释放信号量
dispatch_semaphore_signal(semaphore);
}
总结
本文介绍了iOS开发中常见的五种锁机制:互斥锁、自旋锁、信号量、读写锁和GCD同步锁。对于每种锁机制,我们讲解了它的原理、区别、使用场景和代码示例,以便开发者可以根据实际情况选择合适的锁机制来实现多线程访问控制。在实际开发中,我们需要根据具体的场景选择合适的锁机制,以保证多线程程序的正确性和效率。