【iOS】——锁

五类锁

锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待

锁总共分为五类:

  • 互斥锁
  • 自旋锁
  • 读写锁
  • 条件锁
  • 递归锁

img

互斥锁

在一个多线程环境中,互斥锁可以确保同一时刻只有一个线程能够访问临界区,即共享资源的代码段。

工作原理

  • 在访问共享资源之前进行加锁,访问完成后解锁。

  • 加锁后,任何其他试图加锁的线程会被阻塞,直到当前线程解锁。

  • 解锁时,如果有1个以上的线程阻塞,那么所有该锁上的线程变为就绪状态,第一个就绪的加锁,其他的又进入休眠。

自旋锁

跟互斥类似,只是自旋锁不会引起调用者睡眠, 因为资源被占用的时候, 会一直循环检测锁是否被释放(CPU不能做其他的事情)

节省了唤醒睡眠线程的内核消耗(在加锁时间短暂的情况下会大大提高效率)

在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等

自旋锁和互斥锁的区别

例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而自旋锁则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

  • 互斥锁:当一个线程试图获取一个已经被其他线程持有的锁时,这个线程会被阻塞,直到锁被释放。这通常涉及线程的上下文切换,线程被挂起并让出处理器时间给其他线程或进程。
  • 自旋锁:当一个线程试图获取一个已经被其他线程持有的锁时,这个线程不会被阻塞,而是会持续检查(“自旋”)锁的状态,直到锁被释放。这避免了上下文切换的开销,但在锁持有时间较长时可能导致浪费CPU资源。

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。

**总结:**自旋锁在等待锁时会持续检查锁状态而不放弃CPU时间,适合短期等待;互斥锁在等待时会让出CPU,进入阻塞状态,直至锁可用,适用于长期等待或需要避免CPU空转的场景。

读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。

写的时候:读写都等待;

读的时候:写等待,读无需等待

条件锁

通常与互斥锁一起使用,条件锁允许一个或多个线程等待某个特定条件满足时才继续执行,而互斥锁则用于保护临界区,防止多个线程同时访问共享资源。

工作原理

  1. 等待条件:一个线程调用条件锁的 wait() 方法时,它会释放互斥锁并阻塞自己,直到满足某个条件。在 wait() 被调用期间,其他线程可以获取互斥锁并访问临界区。
  2. 唤醒线程:当条件满足时,通常是由另一个线程调用条件锁的 notify_one()notify_all() 方法来唤醒一个或所有等待的线程。被唤醒的线程将重新尝试获取互斥锁。
  3. 检查条件:被唤醒的线程重新获取互斥锁后,需要再次检查条件是否仍然满足,因为可能有其他线程改变了条件状态。如果条件不满足,线程可能需要再次调用 wait()

递归锁

跟互斥类似, 但是允许同一个线程在未释放锁前,多次加锁, 不会引发死锁

九种锁

在iOS中有九种锁:

  • OSSpinLock
  • os_unfair_lock
  • Dispatch_semaphore
  • pthread_mutex
  • NSLock
  • NSConditon
  • NSRecursiveLock
  • NSConditionLock
  • @synchronize

img

下面是9种锁的耗时排行:

img

OSSpinLock 自旋锁

OSSpinLock是iOS旧版本中提供的一种自旋锁(Spin Lock)实现。它通过忙等待的方式来获取锁,并且不会导致线程的阻塞和切换。自旋锁在iOS 10之后已被标记为废弃,因为它存在优先级反转和性能问题,可以使用os_unfair_lock替代

os_unfair_lock

是一种轻量级的互斥锁,特别适合于那些锁持有时间较短,且锁竞争不激烈的情况。相比于传统的互斥锁(如 pthread_mutex),os_unfair_lock 提供了更低的上下文切换开销

当一个线程试图获取一个已经被占用的锁时,它不会排队等待,而是立即返回,这意味着它可能在其他等待的线程之前再次尝试获取锁。这种机制在低竞争环境下可以减少锁的获取时间,但在高竞争环境下可能导致某些线程长期无法获取锁。

注意⚠️:尽管它在某些方面表现出类似自旋锁的行为(例如,在锁未被其他线程持有时,尝试获取锁的线程不会立即被阻塞),但它本质上仍然是一种互斥锁,因为它确保了同一时刻只有一个线程可以拥有锁。

dispatch_semaphore

  • semaphore叫做”信号量”
  • 信号量的初始值,可以用来控制线程并发访问的最大数量
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
//表示最多开启5个线程
dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);

运行下面代码:

@interface dispatch_semaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation dispatch_semaphoreDemo
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)otherTest
{
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
}
}
- (void)test
{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);

sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);

// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end

在这里插入图片描述

每隔一秒出现一次打印。虽然我们同时开启20个线程,但是一次只能访问一条线程的资源

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态

  • 1、初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL        0
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE        2
#define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL
  • 2、初始化锁
// 初始化锁
pthread_mutex_init(mutex, &attr);
  • 3、初始化锁结束以后,销毁属性
// 销毁属性
pthread_mutexattr_destroy(&attr);
  • 4、加锁解锁
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);
  • 5、销毁锁
pthread_mutex_destroy(&_mutex);

可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性PTHREAD_MUTEX_NORMALpthread_mutex_init(mutex, NULL);

确保在调用 pthread_mutex_lock() 后总是调用相应的 pthread_mutex_unlock(),并且在解锁前不要释放调用线程。否则,可能会导致死锁

NSLock

NSLock是对mutex普通锁的封装

NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name
@end

使用方法:

@interface NSLockDemo()
@property (nonatomic,strong) NSLock *lock;
@end

- (void)test{
[self.lock lock];
[self doSomeThing];
[self.lock unlock];
}

NSRecursiveLock

NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致

@interface RecursiveLockDemo()
@property (nonatomic,strong) NSRecursiveLock *lock;
@end
- (void)test{
[self.lock lock];
[self doSomeThing];
[self.lock unlock];
}

NSCondition

NSCondition是对mutexcond的封装,更加面向对象

@interface NSCondition : NSObject <NSLocking> {
- (void)wait;//用于使当前线程等待,直到接收到信号。 
- (BOOL)waitUntilDate:(NSDate *)limit;//使当前线程等待,直到接收到信号或者指定的日期超时。
- (void)signal;//唤醒一个等待中的线程。
- (void)broadcast;//唤醒所有等待中的线程。
@property (nullable, copy) NSString *name //用于标识 NSCondition 对象的名称
@end

NSConditionwaitsignalbroadcast 方法会自动获取锁,所以你应该在调用这些方法之前先释放锁。这是因为,当你调用 wait 方法时,线程会释放锁并进入等待状态;当你调用 signalbroadcast 方法时,被唤醒的线程会在继续执行前自动获取锁。

#import <Foundation/Foundation.h>

NSCondition *queueCondition = [[NSCondition alloc] init];
NSMutableArray *queue = [NSMutableArray array];

void producer() {
    for (int i = 0; i < 10; i++) {
        [NSThread sleepForTimeInterval:1]; // 模拟耗时操作
        [queueCondition lock];
        [queue addObject:[NSNumber numberWithInt:i]];
        NSLog(@"Producer added item: %d", i);
        [queueCondition signal]; // 唤醒一个等待的消费者
        [queueCondition unlock];
    }
}

void consumer() {
    while (TRUE) {
        [queueCondition lock];
        while ([queue count] == 0) {
            [queueCondition wait]; // 等待生产者添加项目
        }
        NSNumber *item = [queue objectAtIndex:0];
        [queue removeObjectAtIndex:0];
        NSLog(@"Consumer got item: %@", item);
        [queueCondition unlock];
    }
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSThread *producerThread = [[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil];
        NSThread *consumerThread = [[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil];
        
        [producerThread start];
        [consumerThread start];
        
        [producerThread join];
        [consumerThread join];
    }
    return 0;
}
  • 一个生产者线程(producer)生成项目并将其添加到队列中,然后调用 signal 方法唤醒一个等待的消费者线程。
  • 一个消费者线程(consumer)等待队列中有项目可用,一旦有项目可用,它就会从队列中取出项目并处理。

NSCondition 本身并不提供锁的功能,所以需要结合使用 NSLockNSRecursiveLock 来保护临界区

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。它结合了互斥锁和条件变量的功能。NSConditionLock 提供了一种高级的同步机制,允许线程在等待特定条件满足时阻塞自己,而同时保持对共享资源的锁定。

@interface NSConditionLock : NSObject <NSLocking> {
 
- (instancetype)initWithCondition:(NSInteger)condition;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end
  • initWithCondition:初始化Condition,并且设置状态值
  • -[NSConditionLock lock]:获取锁,阻止其他线程访问受保护的资源。
  • -[NSConditionLock unlock]:释放锁,允许其他线程访问受保护的资源。
  • -[NSConditionLock lockWhenCondition:]:如果条件不满足,则释放锁并使当前线程等待,直到条件变为真(true),然后再次获取锁并继续执行。
  • -[NSConditionLock lockWhenCondition:withTimeout:]:与 lockWhenCondition: 相同,但增加了一个超时值,如果超时时间内条件未满足,线程将恢复执行而不等待。
#import <Foundation/Foundation.h>

@interface ConditionDemo : NSObject
@property (nonatomic, strong) NSConditionLock *lock;
@property (nonatomic, assign) int counter;
@end

@implementation ConditionDemo

- (instancetype)init {
    self = [super init];
    if (self) {
        _lock = [[NSConditionLock alloc] initWithCondition:1];
        _counter = 0;
    }
    return self;
}

- (void)incrementCounter {
    [self.lock lock];
    
    if (_counter >= 10) {
        [self.lock lockWhenCondition:1]; // 等待条件满足
    }
    
    _counter++;
    NSLog(@"Counter incremented to: %d", _counter);
    
    [self.lock unlock];
}

- (void)waitForCounterToReachTen {
    [self.lock lock];
    
    while (_counter < 10) {
        [self.lock lockWhenCondition:0]; // 等待条件满足
    }
    
    NSLog(@"Counter reached ten.");
    
    [self.lock unlock];
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ConditionDemo *demo = [[ConditionDemo alloc] init];
        
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        for (int i = 0; i < 20; i++) {
            dispatch_async(queue, ^{
                [demo incrementCounter];
            });
        }
        
        [demo waitForCounterToReachTen];
    }
    return 0;
}

注意

使用 NSConditionLock 的关键点在于正确管理条件的真伪值。通常,你可以使用条件锁的构造函数初始化一个条件值,然后在代码中根据需要更新这个值。当一个线程调用 lockWhenCondition:lockWhenCondition:withTimeout: 方法时,它会检查条件值,如果条件为假(false),则线程会释放锁并等待,直到另一个线程改变了条件值使其为真(true)。

@synchronized

@synchronized是一个编译器指令,对mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

@synchronized([obj]) {
    // 临界区代码
    // 这里的代码将受到保护,一次只允许一个线程执行
}

这里的 [object] 是任何 Objective-C 对象,@synchronized 将使用该对象的地址作为锁的标识。这意味着每次调用 @synchronized 并传入相同的对象时,都会使用同一个锁。如果传入不同的对象,则会使用不同的锁。

工作原理

当一个线程进入由 @synchronized 保护的代码块时,它会尝试获取与传入对象关联的锁。如果锁当前未被其他线程持有,那么该线程将获得锁并执行临界区的代码。如果锁已被其他线程持有,那么当前线程将等待,直到锁被释放。当线程完成临界区的代码执行时,它会自动释放锁。

@synchronized的底层实现就是在开始和结束的时候调用了objc_sync_enter&objc_sync_exit方法。

int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;

if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}

return result;
}

就是根据id2data方法找到一个data对象,然后在对data对象进行mutex.lock()加锁操作。我们点击进入id2data方法继续查找

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

发现获取data对象的方法其实就是根据sDataLists[obj].data这个方法来实现的,也就是一个哈希表。

总之就是@synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值