【iOS】锁

文章目录


前言

iOS中的锁主要可以分为两大类,互斥锁自旋锁,剩下的其他锁其实都是这两种锁的延伸与扩展

我们先用一张对比图来引出我们今天要讲的锁

自旋锁

自旋锁是一种在多处理器系统中被广泛使用的锁机制。它的原理是当一个线程试图获取一个被其他线程持有的锁时,它不是陷入休眠状态,而是一直循环等待(自旋),直到锁被释放为止。

OSSpinLock

这是一个不安全的锁自从OSSpinLock出现安全问题,在iOS10之后就被废弃了。

自旋锁之所以不安全是,因为获取锁后,线程会一直处于忙等待,造成了任务的优先级反转。

首先,我们需要理解自旋锁的工作原理。当一个线程尝试获取已被其他线程占有的自旋锁时,它不会被阻塞,而是在一个小的循环中一直检查锁的状态,直到获取到锁为止 。这种一直运行的机制就被称为"忙等待 "。

忙等待机制可能会导致一个问题,就是优先级反转 。假设有一个低优先级任务持有一个锁,而一个高优先级任务正在等待获取这个锁。通常情况下,高优先级任务应该可以抢占低优先级任务的CPU时间片。但是由于低优先级任务一直在忙等待中运行,它会一直占用CPU时间片 ,而高优先级任务无法抢占,从而导致优先级反转。
也就是使用OSSpinLock会导致低优先级任务先执行

目前OSSpinLock已经被os_unfair_lock替代

os_unfair_lock

由于OSSpinLock并不安全,因此苹果推出了os_unfair_lock以解决优先级反转的问题

bash- 复制代码
//创建一个锁
    os_unfair_lock_t unfairLock;
//初始化
    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    //加锁
    os_unfair_lock_lock(unfairLock);
    //解锁
    os_unfair_lock_unlock(unfairLock);

Demo

bash 复制代码
#import "ViewController.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>

// 全局变量或实例变量
os_unfair_lock_t lock = &(OS_UNFAIR_LOCK_INIT);
@interface ViewController ()

@end


@implementation ViewController

-(void)lowPriorityTask {
    os_unfair_lock_lock(lock);
    NSLog(@"Low priority task started");
    
    // 模拟一些耗时操作
//    [NSThread sleepForTimeInterval:5];
    
    NSLog(@"Low priority task completed");
    os_unfair_lock_unlock(lock);}

// 高优先级任务
-(void)highPriorityTask {
    os_unfair_lock_lock(lock);
    NSLog(@"High priority task started");
    
    // 模拟一些关键操作
    NSLog(@"High priority task completed");
    os_unfair_lock_unlock(lock);}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSThread *lowPriorityThread = [[NSThread alloc] initWithTarget:self selector:@selector(lowPriorityTask) object:nil];
    lowPriorityThread.name = @"Low Priority Thread";
    [lowPriorityThread start];
    
    // 创建高优先级线程
    NSThread *highPriorityThread = [[NSThread alloc] initWithTarget:self selector:@selector(highPriorityTask) object:nil];
    highPriorityThread.name = @"High Priority Thread";
    highPriorityThread.threadPriority = 1.0; // 设置高优先级
    [highPriorityThread start];
}


@end

由此便解决了任务优先级反转的问题

atomic

atomic适用于OC中属性的修饰符,其自带一把自旋锁,但是这个一般基本不使用,都是使用的nonatomic

我们知道属性修饰符不同,settergetter方法会分别调用不同的方法,但是最后会统一调用reallySetProperty

bash 复制代码
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) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

atomic修饰的属性进行了spinlock加锁处理
nonatomic修饰的属性除了没加锁,其他逻辑与atomic一般无二
spinlock_t的底层实现就是os_unfair_lock

当多个线程同时对同一个属性进行读取或写入操作时,使用atomic修饰符可以确保属性的值不会出现异常或不一致的情况。

然而,尽管atomic提供了一定的线程安全性,但它的性能开销较大。因为在多线程同时访问属性时,其他线程需要等待锁的释放,这可能导致性能下降。

尽管atomic提供了一定的线程安全性,但由于性能开销较大,我们在实际开发中更加倾向于nonatomic

互斥锁

互斥锁(Mutex)是一种用于实现线程同步和互斥访问共享资源的基本同步原语。它可以**确保同一时间内,只有一个线程可以获取互斥锁**并访问被保护的临界区代码

我们来看一下自旋锁与互斥锁的区别

  • 互斥锁:如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。
  • 自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待(也就是忙等待),一旦被访问的资源被解锁,则等待资源的线程会立即执行

自旋锁的效率高于互斥锁,但是因为在自旋时不释放CPU,因此持有自旋锁的线程应该尽快释放自旋锁,防止CPU资源消耗,所以自旋锁适合耗时较短的操作

互斥锁又分为两种

  • 递归锁
    允许同一个线程多次获取同一个锁
  • 非递归锁
    不允许同一个线程多次获取同一个锁

互斥锁的实现细节:

大多数操作系统都提供了互斥锁的底层实现,如POSIX线程库中的pthread_mutex。在iOS/macOS系统中,常用的互斥锁包括:

  • pthread_mutex: POSIX线程库中的互斥锁,是底层实现。
  • NSLock: Objective-C中的互斥锁,内部使用pthread_mutex实现。
  • @synchronized: Objective-C中的另一种互斥锁实现,基于对象监视器(monitor)。
    接下来我们具体讲讲这些互斥锁

pthread_mutex

pthread_mutex就是互斥锁本身,当锁被占用,其他线程申请锁时,不会一直忙等待,而是阻塞线程并睡眠

bash 复制代码
// 导入头文件
#import <pthread.h>

// 全局声明互斥锁
pthread_mutex_t _lock;

// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);

// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// 解锁 
pthread_mutex_unlock(&_lock);

// 释放锁
pthread_mutex_destroy(&_lock);

@synchronized

@synchronized是日常开发比较常用的一种锁,它的使用比较简单,但是性能较低

我们通过源码理解一下这个锁

在Objective-C Runtime源码中,@synchronized由objc-sync.mm文件中的几个函数实现。核心实现在objc_sync_enterobjc_sync_exit两个函数中。

bash 复制代码
// 进入临界区
id 
objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        SyncData_t *sd = sdallow(obj, false, false);
        result = sdold_sync_enter(sd);
    }
    return (result == OBJC_SYNC_SUCCESS) ? obj : nil;
}

// 退出临界区
void
objc_sync_exit(id obj)
{
    if (obj) {
        SyncData_t *sd = sdallow(obj, true, true);
        sdold_sync_exit(sd);
    }
}

这两个函数的主要作用是进入和退出@synchronized临界区。它们的关键在于SyncData_t结构体和sdallow函数。

  • SyncData_t结构体定义如下:
bash 复制代码
struct SyncData_t {
    recursive_mutex_t mutex; // 互斥锁
    uintptr_t          value;  // 关联对象存储的值
    uintptr_t          exception; 
};

SyncData_t结构体中包含了一个recursive_mutex_t类型的互斥锁,这是@synchronized底层实现同步的关键,这也说明@synchronized其实是一个递归互斥锁

  • sdallow函数的作用是获取与给定对象关联的SyncData_t实例,如果不存在就创建一个新的。这个函数的实现利用了关联对象(AssociatedObject)技术,将SyncData_t实例与特定对象关联存储。
bash 复制代码
SyncData_t *
sdallow(id obj, bool create, bool warmup)
{
    SyncData_t *sd = sdalloc(obj->isTaggedPointer(), create);
    // ...
    return sd;
}

SyncData_t *
sdalloc(bool tagged, bool create)
{
    SyncData_t *sd;
    if (tagged) {
        sd = (SyncData_t *)&SyncDataObjc_Tag; // Tagged pointer objects
    } else {
        sd = (SyncData_t *)object_getAssociatedObject(obj, &SyncDataAQL); // 获取关联对象
        if (sd == nil && create) {
            sd = (SyncData_t *)calloc(1, sizeof(SyncData_t)); // 创建新实例
            object_setAssociatedObject(obj, &SyncDataAQL, sd, OBJC_ASSOCIATION_ASSIGN); // 设置关联对象
        }
    }
    return sd;
}

总结

  • 因为@synchronized在底层封装的是一把递归锁 ,所以这个锁是递归互斥锁
  • @synchronized的原因是为了方便下一个data的插入
  • 由于链表的查询与缓存的查找十分消耗性能,因此该锁的性能排名比较低下
  • 但是因为使用起来方便简单,不用解锁,使用率还是比较高的
  • 不能用非OC对象作为加锁对象,因为object的参数为id
  • @synchronized (self)这种适用于嵌套次数较少的场景。这里锁住的对象也并不永远是self,这里需要读者注意

NSLock

我们来看一下NSLock的底层实现

NSLock的实现位于Foundation框架的NSLock.m文件中,它的核心数据结构是一个名为_NSLockingData的结构体

bash 复制代码
typedef struct _NSLockingData {
    pthread_mutex_t mutex; // POSIX线程互斥锁
    volatile int32_t val; // 锁的状态值
    volatile int32_t contenders; // 等待获取锁的线程数
    uint16_t behavior; // 锁的行为选项
    uint16_t reserved;
} _NSLockingData;

可以看到pthread_mutex_t 类型的互斥锁包含在我们的结构体中

所以我们可以知道他是一个非递归互斥锁

在使用非递归锁时,如果发生了递归调用,线程不会死锁,而是会被阻塞(堵塞)

bash 复制代码
- (void)test {
    self.testArray = [NSMutableArray array];
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            self.testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
}

从官方文档的解释里看的更清楚,在同一线程上调用NSLock的两次lock方法将永久锁定线程。同时官方文档重点提醒向NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。

NSRecursiveLock

NSRecursiveLockObjective-C中的递归锁类,它是基于底层的pthread_mutex_t实现的 ,同时是 NSLock 的子类 ,具有相同的基本功能,但允许同一个线程多次对锁进行加锁操作而不会造成死锁

NSRecursiveLock使用了一个互斥锁(mutex lock)来实现递归锁的功能。互斥锁是一种同步机制,它提供了对临界区的独占访问

我们来看一下官方文档中的解释

他是一个同一线程可以多次获取而不会导致死锁的锁 ,重点是在同一线程。

我们来看一个可能会导致递归锁死锁的例子

这个示例中,我们将创建两个线程,每个线程都试图在获取了一个锁之后获取另一个锁,但是锁的获取顺序相反,这就可能导致死锁:

bash 复制代码
- (void)recursiveLockDeadlockExample {
    NSRecursiveLock *lockA = [[NSRecursiveLock alloc] init];
    NSRecursiveLock *lockB = [[NSRecursiveLock alloc] init];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [lockA lock];
        NSLog(@"Thread 1 acquired lock A");
        sleep(1); // Wait to ensure Thread 2 locks lock B
        [lockB lock];
        NSLog(@"Thread 1 acquired lock B");
        [lockB unlock];
        [lockA unlock];
        NSLog(@"Thread 1 released both locks");
    });

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        [lockB lock];
        NSLog(@"Thread 2 acquired lock B");
        sleep(1); // Wait to ensure Thread 1 locks lock A
        [lockA lock];
        NSLog(@"Thread 2 acquired lock A");
        [lockA unlock];
        [lockB unlock];
        NSLog(@"Thread 2 released both locks");
    });
}

• 线程1 首先获取 lockA,然后尝试获取 lockB

• 同时,线程2 首先获取 lockB,然后尝试获取 lockA

如果两个线程几乎同时运行,线程1 成功获取 lockA 后,线程2 获取了 lockB。此时,线程1 在尝试获取 lockB 时会被阻塞 ,因为它已经被线程2 持有;同样,线程2 在尝试获取 lockA 时也会被阻塞 ,因为它已经被线程1持有。这样就形成了一个典型的死锁情况。

也就是多线程不能同时获取同一把互斥锁,不然可能导致死锁

条件锁

条件锁是一种特殊类型的同步机制,用于管理不同线程间的执行顺序,使得某些线程能在满足特定条件时才继续执行。

NSCondition封装了pthread_mutex的以上几个函数,NSConditionLock封装了NSCondition

NSCondition

NSCondition 是一个条件锁,它允许线程在满足某个条件之前挂起,直到其他线程改变了条件并通知 NSCondition

通俗的理解就是当当前线程不满足条件时阻塞该线程,直到其他线程通知可以继续被阻塞的线程可以继续往下走

NSCondition的对象实际上作为一个锁 和 一个线程检查器

  • 锁主要为了当检测条件时保护数据源,执行条件引发的任务
  • 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞

看一个经典的生产-消费例子

bash 复制代码
- (void)producer {
    while (true) {
        [_condition lock];
        // 生产一个产品
        self.ticketconuts++;
//        NSLog(@"Produced a product");
        NSLog(@"生产一个 现有 count %d",self.ticketconuts);

        [_condition signal]; // 通知消费者
        [_condition unlock];
//        sleep(1); // 模拟生产时间
    }
}

- (void)consumer {
    while (true) {
        [_condition lock];
        while (self.ticketconuts == 0) {
            [_condition wait]; // 等待产品可用
            NSLog(@"等待");
        }
        self.ticketconuts--;
//        NSLog(@"Consumed a product");
        NSLog(@"消费一个 现有 count %d",self.ticketconuts);
        [_condition unlock];
//        sleep();
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.ticketconuts = 50;
    _condition = [[NSCondition alloc] init];  
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self producer];
        });
    }
}

如果产品不足就用wait告诉当前线程需要等待,也就是阻塞线程,直到signal通知线程可以继续往下走

  • (void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。
  • (void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。
  • (void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。
  • (void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。

NSLock NSRecursiveLock NSCondition总结

  • NSLock不支持递归加锁
  • NSRecursiveLock虽然有递归性,但没有多线程特性
  • NSCondition 的对象实际上作为⼀个锁和⼀个线程检查器

NSConditionLock

NSConditionLock也是条件锁,一旦一个线程获取该锁,其他想获取该锁的线程一定等待

NSConditionLock是对NSCondition的封装

NSConditionLock 提供了一系列方法来支持基于条件的锁操作:

initWithCondition::初始化一个带有特定初始状态的 NSConditionLock
lockWhenCondition::只有当锁的状态与指定的条件匹配 时,调用该方法的线程才能获取锁 。如果状态不匹配,线程将阻塞,直到状态变为所需的条件。
tryLockWhenCondition::尝试获取锁,只有当锁的状态与指定条件匹配时才会成功。如果无法立即获取锁,方法将返回 NO 而不会阻塞线程。
unlockWithCondition::释放锁并将其状态设置为一个新的值。这对于在多个线程间同步状态转换非常有用。

用一个Demo来示例NSConditionLock的使用

bash 复制代码
- (void)cjl_testConditonLock{
    // 信号量
    NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
         [conditionLock lockWhenCondition:1]; // conditoion = 1 内部 Condition 匹配
        // -[NSConditionLock lockWhenCondition: beforeDate:]
        NSLog(@"线程 1");
         [conditionLock unlockWithCondition:0];
    });
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
       
        [conditionLock lockWhenCondition:2];
        sleep(0.1);
        NSLog(@"线程 2");
        // self.myLock.value = 1;
        [conditionLock unlockWithCondition:1]; // _value = 2 -> 1
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
       [conditionLock lock];
       NSLog(@"线程 3");
       [conditionLock unlock];
    });
}

由于队列优先级的原因,线程的调用顺序就是:线程1->线程3->线程2,由于线程1由于条件不符合不会先执行,即使他的优先级是最高的,然后线程3执行任务,执行线程3由于条件符合执行线程2,线程2改变条件后线程1开始执行

demo分析汇总

线程 1 调用[NSConditionLock lockWhenCondition:],此时此刻因为不满足当前条件,所以会进入

waiting 状态,当前进入到 waiting 时,会释放当前的互斥锁。

此时当前的线程 3 调用[NSConditionLock lock:],本质上是调用 [NSConditionLock

lockBeforeDate:],这里不需要比对条件值,所以线程 3 会打印

接下来线程 2 执行[NSConditionLock lockWhenCondition:],因为满足条件值,所以线程2

会打印,打印完成后会调用[NSConditionLock unlockWithCondition:],这个时候将value 设置为

1,并发送 boradcast, 此时线程 1 接收到当前的信号,唤醒执行并打印。

自此当前打印为 线程 3->线程 2 -> 线程 1

NSConditionLock lockWhenCondition:\];这里会根据传入的 condition 值和 Value 值进行对比,如果不相等,这里就会阻塞,进入线程池,否则的话就继续代码执行\[NSConditionLock unlockWithCondition:\]: 这里会先更改当前的 value 值,然后进行广播,唤醒当前的线程

总结

  • OSSpinLocK自旋锁由于性能问题,底层已经用os_unfair_lock替代,这是一种非常高效的锁,用于替代不再推荐使用的 OSSpinLock。它避免了忙等,而是使线程休眠,但在锁竞争激烈的情况下可以保持高性能。但推荐执行时间较短的任务使用,否则会造成CPU资源消耗
  • atomic原子锁自带一般自旋锁,在使用getter与setter方法时底层实现会进行加锁保证安全,但是比较消耗性能,日常开发还是更推荐nonatomic
  • @synchronized在底层使用哈希表来维护 每个锁对象相关的线程数据,同时通过链表来记录锁的获取情况,虽然性能较低,但是使用方便,使用率高。但是同时由于它是递归锁,需要进行额外的计数管理,增加了运行时的开销
  • NSLockNSRecursiveLock是对pthread_mutex的封装,NSLock是互斥锁,NSRecursive是递归互斥锁
  • NSConditionNSConditionLock是条件锁,底层都是对pthread_mutex的封装
相关推荐
吴佳浩1 天前
OpenClaw macOS 完整安装与本地模型配置教程(实战版)
人工智能·macos·agent
开心就好20252 天前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好20252 天前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao3 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy4 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy4 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode4 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农4 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
vi_h5 天前
在 macOS 上通过 Docker 安装并运行 Ollama(详细可执行教程)
macos·docker·ollama
iceiceiceice5 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios