当前位置: 首页 > news >正文

【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:“条件等待锁”(生产者-消费者型)

原理NSConditionpthread_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

锁的选择:

  1. 性能要求高:优先选 dispatch_semaphore_t(通用)或 os_unfair_lock(轻量同步)。
  2. 需要递归:选 pthread_mutex(recursive)NSRecursiveLock
  3. 代码简洁性:选 @synchronized(适合小范围同步)。
  4. 条件等待:选 NSCondition(如生产者-消费者模型)。

4️⃣常见陷阱

4.1 死锁(Deadlock)

原因:多个线程互相等待对方释放锁(如线程 A 持有锁 1 等待锁 2,线程 B 持有锁 2 等待锁 1)。

解决

  • 避免嵌套加锁(如非必要不使用递归锁)。
  • 统一加锁顺序(所有线程按相同顺序获取锁)。

4.2 锁内执行耗时操作

原因:锁的加锁/解锁涉及内核调用,若临界区内执行耗时操作(如 IO、大量计算),会导致其他线程长时间等待,降低并发效率。

解决

  • 将耗时操作移到锁外(如先读取数据到临时变量,再在锁内处理)。

4.3 错误使用锁对象

原因@synchronized使用动态对象(如可能被释放的 self)作为锁标识,或不同线程使用不同的锁对象。

解决

  • @synchronized的锁对象需是生命周期稳定的(如专用 NSObject实例)。

总结

OC 中的锁机制需根据场景选择:

  • 轻量同步dispatch_semaphore_tos_unfair_lock(性能最优)。
  • 递归需求pthread_mutex(recursive)NSRecursiveLock
  • 代码简洁@synchronized(小范围使用)。
  • 条件协调NSCondition(如生产者-消费者)。

核心原则:锁的范围尽可能小(仅保护临界区),避免死锁,优先使用系统提供的高性能锁(如 GCD 信号量)。

http://www.xdnf.cn/news/1161505.html

相关文章:

  • 归并排序:优雅的分治排序算法(C语言实现)
  • Spring Boot05-热部署
  • 设计模式六:工厂模式(Factory Pattern)
  • Trae开发uni-app+Vue3+TS项目飘红踩坑
  • 数据结构自学Day11-- 排序算法
  • 迁移科技3D视觉系统:赋能机器人上下料,开启智能制造高效新纪元
  • react-window 大数据列表和表格数据渲染组件之虚拟滚动
  • GoLang教程005:switch分支
  • Git核心功能简要学习
  • 面试总结第54天微服务开始
  • Neo4j graph database
  • 【数据结构与算法】数据结构初阶:详解二叉树(二)——堆
  • Vue3 面试题及详细答案120道 (1-15 )
  • Node.js的Transform 流
  • 2x2矩阵教程
  • 亚马逊自养号测评实战指南:从环境搭建到安全提排名
  • G1垃圾回收器
  • 复习博客:JVM
  • LVS 集群技术基础
  • Valgrind Cachegrind 全解析:用缓存效率,换系统流畅!
  • 【初识数据结构】CS61B中的最小生成树问题
  • 本地部署Nacos开源服务平台,并简单操作实现外部访问,Windows 版本
  • ZooKeeper学习专栏(四):单机模式部署与基础操作详解
  • ruoyi-flowable-plus Excel 导入数据 Demo
  • 【qml-3】qml与c++交互第二次尝试(类型方式)
  • (9)机器学习小白入门 YOLOv:YOLOv8-cls 技术解析与代码实现
  • uni-app 开发小程序项目中实现前端图片压缩,实现方式
  • Java基础面试题
  • Laravel 后台登录 403 Forbidden 错误深度解决方案-优雅草卓伊凡|泡泡龙
  • 芯谷科技--固定电压基准双运算放大器D4310