【iOS】锁[特殊字符]
文章目录
- 前言
- 1️⃣什么是锁🔒?
- 1.1 基本概念
- 1.2 锁的分类
- 2️⃣OC 中的常用锁
- 2.1 OSSpinLock(已弃用):“自旋锁”的经典代表
- 为什么尽量在开发中不使用自旋锁
- 自旋锁的本质缺陷:忙等待(Busy Waiting)
- os_unfair_lock的局限性:不适用于复杂场景
- 苹果的官方建议:优先使用更高效的锁
- 2.2 dispatch_semaphore_t(GCD 信号量):“高性能通用锁”
- 2.3 pthread_mutex(POSIX 互斥锁):“最通用的系统级锁”
- 2.4 NSLock:“OC 层面的互斥锁封装”
- 2.5 @synchronized:“OC 特有的语法糖”
- 2.6 NSCondition:“条件等待锁”(生产者-消费者型)
- 3️⃣锁的性能对比与选择建议
- 4️⃣常见陷阱
- 4.1 死锁(Deadlock)
- 4.2 锁内执行耗时操作
- 4.3 错误使用锁对象
- 总结
前言
在前段时间学习dyld时,接触到了锁,由于并不是很清楚锁的知识,导致涉及到锁的其他内容也有点懵懂,再加上,想要深入了解OC中的多线程,锁是前提预备知识,所以笔者对锁进行简单学习并撰写了这篇博客。
1️⃣什么是锁🔒?
1.1 基本概念
百度解释如下:
锁是编程中用于协调多个线程或进程对共享资源访问的机制,主要用于防止并发冲突、确保数据一致性和程序正确性。
互斥性:保证同一时刻只有一个线程/进程访问共享资源,避免数据竞争。
协调线程/进程的执行顺序,确保操作原子性(全部完成或全部不完成)。
简单来说,锁就像一个“开关”,在同一时间只允许一个线程“进入”某个代码段或访问某个资源,其他线程需要等待,直到锁被释放。
1.2 锁的分类
根据分类标准,一般情况下我们把锁分为一下7大类别:
(1)悲观锁和乐观锁
(2)公平锁和非公平锁
(3)共享锁和独占锁
(4)可重入锁和非可重入锁
(5)自旋锁和非自旋锁
(6)偏向锁、轻量级锁和重量级锁
(7)可中断锁和不可中断锁
在OC中,锁的大类就两种自旋锁和互斥锁,可以细分为以下几类:
类型 | 特点 | 典型代表 |
---|---|---|
互斥锁(Mutex) | 同一时间仅允许一个线程加锁,不可重入 | pthread_mutex、NSLock |
自旋锁 | 线程会反复检查变量是否可用 | OSSpinLock(已弃用)、atomic |
条件锁(Condition) | 等待特定条件满足后再加锁 | NSCondition、pthread_cond_t |
递归锁(Recursive Lock) | 允许同一线程多次加锁(需对应次数解锁) | pthread_mutex(recursive)、NSRecursiveLock |
信号量(Semaphore) | 控制并发线程数量(非严格互斥) | dispatch_semaphore_t |
语言特性锁 | OC 语法糖,简化锁操作 | @synchronized(互斥锁) |
另外还有一个读写锁:读写锁实际是一种特殊的自旋锁。将对共享资源的访问分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性。
2️⃣OC 中的常用锁
2.1 OSSpinLock(已弃用):“自旋锁”的经典代表
原理:自旋锁(Spin Lock)通过忙等待(循环检查锁状态)实现互斥。当锁被占用时,其他线程不会休眠,而是不断循环尝试获取锁,直到成功。
历史:OSSpinLock 曾是 OC 中最快的锁(无系统调用,纯用户态操作),但因 优先级反转问题(低优先级线程持有锁时,高优先级线程会疯狂自旋抢占 CPU,导致系统卡顿)在 iOS 10 后被弃用。
示例代码(仅作原理参考,禁止在生产环境使用):
#import <os/lock.h>os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // OSSpinLock 已弃用,替代方案是 os_unfair_lock(本质是改良的自旋锁)- (void)threadSafeMethod {os_unfair_lock_lock(&lock);// 临界区:访问共享资源os_unfair_lock_unlock(&lock);
}
注意:os_unfair_lock是 OSSpinLock 的替代方案,但仍为自旋锁,适用于轻量级同步(如单次数据访问),避免在锁内执行耗时操作。
为什么尽量在开发中不使用自旋锁
自旋锁的本质缺陷:忙等待(Busy Waiting)
os_unfair_lock的底层实现与被弃用的 OSSpinLock类似,均基于自旋锁机制。当锁被其他线程占用时,当前线程会进入一个 无限循环(忙等待),不断检查锁是否释放。这种机制会导致以下问题:
1.cpu资源的浪费
自旋锁的“忙等待”会持续占用 CPU 时间片,即使线程并未执行任何有效操作。在高负载场景(如多线程竞争激烈)下,大量线程空转会导致 CPU 使用率飙升,甚至引发系统卡顿。
示例对比:
- 传统互斥锁(如 pthread_mutex):锁被占用时,线程会主动休眠(释放 CPU),等待锁释放后被唤醒。
- 自旋锁(如 os_unfair_lock):锁被占用时,线程持续空转,CPU 资源被无意义消耗。
2.优先级反转
当低优先级的线程持有锁时,高优先级线程会因不断尝试获取锁而频繁抢占cpu,导致低优先级线程无法运行(被“饿死”)。这种现象被称为优先级反转,会严重破坏系统的实时性和公平性。
eg:低优先级线程 A 持有 os_unfair_lock。高优先级线程 B 尝试获取锁,进入忙等待,持续占用 CPU。系统被迫频繁调度高优先级线程 B,低优先级线程 A 无法获得执行机会。
os_unfair_lock的局限性:不适用于复杂场景
尽管 os_unfair_lock比 OSSpinLock更轻量(减少了部分内存屏障),但它的设计目标仅限于 轻量级、短时间的临界区保护(如单次内存访问)。对于以下场景,它无法提供可靠支持:
1. 长时间持有锁的操作
如果临界区内需要执行耗时操作(如文件 I/O、网络请求、复杂计算),os_unfair_lock
的忙等待会导致线程长时间占用 CPU,严重影响其他任务的执行效率。
eg:
// 错误用法:临界区执行耗时操作(如读取大文件)
os_unfair_lock_lock(&lock);
NSData *largeData = [NSData dataWithContentsOfFile:@"/bigfile.dat"]; // 耗时操作
os_unfair_lock_unlock(&lock);
此时,线程会因忙等待导致 CPU 空转,而文件读取的 I/O 操作本身是阻塞的(无需 CPU 参与),造成资源浪费。
2.递归锁需求
os_unfair_lock不支持递归加锁(同一线程无法多次获取同一把锁)。如果代码中存在递归调用(如方法 A 调用方法 B,两者都需要加同一把锁),会导致死锁。
eg:
- (void)recursiveMethod {os_unfair_lock_lock(&lock);// 递归调用自身[self recursiveMethod]; // 第二次加锁会失败,导致死锁os_unfair_lock_unlock(&lock);
}
苹果的官方建议:优先使用更高效的锁
苹果在官方文档中明确推荐,避免使用自旋锁(包括 os_unfair_lock)处理需要长时间持有或高竞争的场景,并提供了更优的替代方案:
1. dispatch_semaphore_t(GCD 信号量)
基于内核信号量实现,锁被占用时线程会休眠(释放 CPU),支持并发控制(计数 >1 时允许多线程同时访问)。适用于轻量级同步、限制并发任务数(如限制同时下载的文件数)等场景。
2. pthread_mutex(POSIX 互斥锁)
支持递归锁(通过 PTHREAD_MUTEX_RECURSIVE类型),内核级实现,稳定性高。适用于需要递归加锁或多线程频繁竞争的场景(如嵌套方法调用)。
3. NSLock(OC 层面封装)
API 简洁,基于 pthread_mutex实现,支持 tryLock非阻塞加锁。适用于简单的线程同步(如保护单次数据访问)。
2.2 dispatch_semaphore_t(GCD 信号量):“高性能通用锁”
信号量(Semaphore)是基于计数器的一种多线程同步机制,用来管理对资源的并发访问。信号量就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
相关函数:
-
dispatch_semaphore_t、dispatch_semaphore_create(long value):创建信号量,初始化计数(通常为 1 时表示互斥锁)。参数为信号量的初值,小于零就会返回NULL。
-
线程加锁时调用 dispatch_semaphore_wait减少计数(若计数为 0 则阻塞)。long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); :等待降低信号量,接收一个信号和时间值(多为DISPATCH_TIME_FOREVER)。若信号的信号量为0,则会阻塞当前线程,直到信号量大于0或者经过输入的时间值。若信号量大于0,则会使信号量减1并返回,程序继续住下执行。
-
解锁时调用 dispatch_semaphore_signal增加计数(唤醒等待线程)。long dispatch_semaphore_signal(dispatch_semaphore_t dsema); :提高信号量, 使信号量加1并返回 在dispatch_semaphore_wait和dispatch_semaphore_signal这两个函数中间的执行代码,每次只会允许限定数量的线程进入,这样就有效的保证了在多线程环境下,只能有限定数量的线程进入。
特点:
- 性能接近 OSSpinLock(底层通过内核信号量实现,但比传统锁更高效)。
- 支持控制并发线程数量(计数 >1 时为“并发锁”,允许指定数量线程同时访问)。
示例代码:
/*
// 初始化信号量(计数为 1,即互斥锁)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);- (void)threadSafeMethod {// 加锁:计数减 1(若计数为 0 则阻塞当前线程)dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);// 临界区:访问共享资源// 解锁:计数加 1(唤醒等待线程)dispatch_semaphore_signal(semaphore);
}
*///具体实例
#import <Foundation/Foundation.h>
#import <os/lock.h>int main(int argc, const char * argv[]) {@autoreleasepool {// 共享资源:一个需要线程安全的计数器__block int counter = 0;// 初始化信号量(计数为 1,即互斥锁;若计数>1,允许指定数量线程同时访问)dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);// 模拟 10 个线程同时修改计数器for (int i = 0; i < 10; i++) {dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{for (int j = 0; j < 1000; j++) {// 加锁:计数减 1(若计数为 0 则阻塞)dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);// 临界区:修改共享资源(需保证原子性)counter++;// 解锁:计数加 1(唤醒等待线程)dispatch_semaphore_signal(semaphore);}});}// 等待所有线程完成后打印结果(实际开发中需用更严谨的同步机制)sleep(2);NSLog(@"最终计数器值:%d", counter); // 应输出 10000(10 线程×1000 次)}return 0;
}
运行测试:
适用场景:
- 需要高性能互斥的场景(如高频数据读写)。
- 控制并发任务数量(如限制同时下载的任务数,计数设为 3)。
2.3 pthread_mutex(POSIX 互斥锁):“最通用的系统级锁”
POSIX 标准的互斥锁(Mutex),通过内核实现线程阻塞。支持两种类型:
- 普通锁(PTHREAD_MUTEX_INITIALIZER):不可重入,同一线程重复加锁会死锁。
- 递归锁(PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP):允许同一线程多次加锁(需对应次数解锁)。
示例代码:
/*
#import <pthread.h>// 初始化递归锁(允许同一线程多次加锁)
pthread_mutex_t recursiveLock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 设置为递归锁
pthread_mutex_init(&recursiveLock, &attr);- (void)recursiveMethod {pthread_mutex_lock(&recursiveLock);// 临界区(可能递归调用自身)pthread_mutex_unlock(&recursiveLock);
}// 销毁锁(避免内存泄漏)
pthread_mutex_destroy(&recursiveLock);
pthread_mutexattr_destroy(&attr);
*///具体实例
#import <Foundation/Foundation.h>
#import <os/lock.h>
#import <pthread/pthread.h>int main(int argc, const char * argv[]) {@autoreleasepool {// 共享资源:一个需要线程安全的数组__block NSMutableArray *sharedArray = [NSMutableArray array];// 初始化递归锁(允许同一线程多次加锁)pthread_mutex_t recursiveLock;pthread_mutexattr_t attr;pthread_mutexattr_init(&attr);pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // 关键:设置为递归锁pthread_mutex_init(&recursiveLock, &attr);// 线程 1:嵌套调用方法dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{pthread_mutex_lock(&recursiveLock); // 第一次加锁[sharedArray addObject:@"A"];// 嵌套调用(需再次加锁)pthread_mutex_lock(&recursiveLock); // 第二次加锁(递归锁允许)[sharedArray addObject:@"B"];pthread_mutex_unlock(&recursiveLock); // 第一次解锁pthread_mutex_unlock(&recursiveLock); // 第二次解锁});// 线程 2:直接访问共享资源dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{pthread_mutex_lock(&recursiveLock);[sharedArray addObject:@"C"];pthread_mutex_unlock(&recursiveLock);});// 等待线程完成后打印结果sleep(2);NSLog(@"共享数组:%@", sharedArray); // 应输出 ["A", "B", "C"](顺序可能不同)}return 0;
}
运行测试:
特点:
- 性能稳定,兼容所有 iOS 版本。
- 递归锁适合嵌套调用场景(如方法 A 调用方法 B,两者都需要加同一把锁)。
2.4 NSLock:“OC 层面的互斥锁封装”
原理:NSLock是 pthread_mutex的 OC 封装,提供了更简洁的 API(如 lock、unlock、tryLock)。默认是普通锁(不可重入),但可通过 NSRecursiveLock子类实现递归锁。
示例代码:
/*
// 普通互斥锁
NSLock *lock = [[NSLock alloc] init];- (void)threadSafeMethod {[lock lock];// 临界区[lock unlock];
}// 尝试加锁(非阻塞,返回 BOOL 表示是否成功)
if ([lock tryLock]) {// 临界区[lock unlock];
}// 递归锁(允许同一线程多次加锁)
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
*///具体实例
#import <Foundation/Foundation.h>
#import <os/lock.h>
#import <pthread/pthread.h>int main(int argc, const char * argv[]) {@autoreleasepool {// 共享资源:一个需要线程安全的用户信息字典__block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];// 初始化普通互斥锁NSLock *lock = [[NSLock alloc] init];// 创建一个调度组dispatch_group_t group = dispatch_group_create();// 线程 1:修改用户信息dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[lock lock]; // 加锁userInfo[@"name"] = @"张三";userInfo[@"age"] = @25;[lock unlock]; // 解锁});// 线程 2:读取用户信息(需等待线程 1 解锁)dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{[lock lock]; // 加锁(若线程 1 未解锁会阻塞)NSString *name = userInfo[@"name"];NSNumber *age = userInfo[@"age"];NSLog(@"用户信息:%@,%@", name, age); // 应输出 "张三",25[lock unlock]; // 解锁});// 等待所有异步任务完成dispatch_group_wait(group, DISPATCH_TIME_FOREVER);}return 0;
}
运行测试:
注意:NSLock的 unlock必须与 lock成对出现,否则可能导致死锁(如异常未捕获导致 unlock未执行)。
2.5 @synchronized:“OC 特有的语法糖”
原理:@synchronized是 OC 编译器提供的语法糖,底层通过哈希表将锁对象映射到内核互斥锁。语法简洁,无需手动管理锁的创建和销毁。
示例代码:
// 以某个对象(如 self)为锁的标识
- (void)threadSafeMethod {@synchronized (self) { // 锁对象为 self// 临界区:访问共享资源}
}// 也可以用其他对象作为锁(推荐专用锁对象,避免与其他代码冲突)
NSObject *lockObj = [[NSObject alloc] init];
@synchronized (lockObj) {// 临界区
}
特点:
- 代码简洁,无需手动加锁/解锁(自动管理)。
- 锁对象需唯一(不同锁对象无法同步)。
- 性能较差(底层涉及哈希表查找和内核调用),适合小范围同步(如单次数据访问)。
2.6 NSCondition:“条件等待锁”(生产者-消费者型)
原理:NSCondition
是 pthread_cond_t
的 OC 封装,结合了互斥锁和条件变量。允许线程在条件不满足时等待(释放锁并休眠),条件满足时被唤醒(重新加锁)。
示例代码(生产者-消费者模型):
@interface ProducerConsumer : NSObject {NSMutableArray *_queue;NSCondition *_condition;NSInteger _maxCount;
}
@end@implementation ProducerConsumer
- (instancetype)init {if (self = [super init]) {_queue = [NSMutableArray array];_condition = [[NSCondition alloc] init];_maxCount = 10; // 队列最大容量}return self;
}// 生产者:添加数据(队列满时等待)
- (void)produce {[_condition lock];while (_queue.count >= _maxCount) {[_condition wait]; // 条件不满足,释放锁并休眠}[_queue addObject:@(arc4random_uniform(100))];[_condition signal]; // 唤醒一个等待的消费者[_condition unlock];
}// 消费者:取出数据(队列空时等待)
- (void)consume {[_condition lock];while (_queue.count == 0) {[_condition wait]; // 条件不满足,释放锁并休眠}id obj = _queue.firstObject;[_queue removeObjectAtIndex:0];[_condition signal]; // 唤醒一个等待的生产者[_condition unlock];
}
@end
适用场景:需要线程间协调(如“生产者-消费者”模型),等待特定条件满足后再执行。
3️⃣锁的性能对比与选择建议
OC 中常见锁的性能(从高到低,单线程加锁/解锁耗时):
os_unfair_lock ≈ dispatch_semaphore_t > pthread_mutex(递归) > NSRecursiveLock > NSLock > @synchronized
锁的选择:
- 性能要求高:优先选
dispatch_semaphore_t
(通用)或os_unfair_lock
(轻量同步)。 - 需要递归:选
pthread_mutex(recursive)
或NSRecursiveLock
。 - 代码简洁性:选
@synchronized
(适合小范围同步)。 - 条件等待:选
NSCondition
(如生产者-消费者模型)。
4️⃣常见陷阱
4.1 死锁(Deadlock)
原因:多个线程互相等待对方释放锁(如线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1)。
解决:
- 避免嵌套加锁(如非必要不使用递归锁)。
- 统一加锁顺序(所有线程按相同顺序获取锁)。
4.2 锁内执行耗时操作
原因:锁的加锁/解锁涉及内核调用,若临界区内执行耗时操作(如 IO、大量计算),会导致其他线程长时间等待,降低并发效率。
解决:
- 将耗时操作移到锁外(如先读取数据到临时变量,再在锁内处理)。
4.3 错误使用锁对象
原因:@synchronized
使用动态对象(如可能被释放的 self
)作为锁标识,或不同线程使用不同的锁对象。
解决:
@synchronized
的锁对象需是生命周期稳定的(如专用NSObject
实例)。
总结
OC 中的锁机制需根据场景选择:
- 轻量同步:
dispatch_semaphore_t
或os_unfair_lock
(性能最优)。 - 递归需求:
pthread_mutex(recursive)
或NSRecursiveLock
。 - 代码简洁:
@synchronized
(小范围使用)。 - 条件协调:
NSCondition
(如生产者-消费者)。
核心原则:锁的范围尽可能小(仅保护临界区),避免死锁,优先使用系统提供的高性能锁(如 GCD 信号量)。