iOS 锁

概述

在开发过程中,使用多线程来可以提高程序运行效率。本文不说多线程,重点说说锁的使用。

什么时候需要用到锁呢?

比如相亲,多少单身狗的痛。你经过七大姑八大姨的介绍,争取到了一个相亲的机会,于是你就屁颠屁颠的去见人家姑娘了。结果殊不知,等你到了人家姑娘的家中后,发现她正在和另一个同学相谈甚欢,这个时候你能进去见人家姑娘吗?显然不能。可能她的妈妈就在门口看着呢。此处的妈妈的职责就是保证正在进行相亲不会因为其他相亲者到来而被中断,此时,你应该怎么办,等呗!你必须等那位同学相亲走了,你才可以进去和人家闺女相亲。

iOS 中的锁就相当于妈妈,当一个线程访问数据的时候,其他的线程就必须等到该线程访问完毕以后才可访问。

如果在同一时刻多个线程对同一数据进行访问,会出现不可预期的结果,这就造成了线程不安全。比如,一个线程正在写入数据,如果这个时候有另一个线程来读取数据,那么获取到的数据将是不可预期的,也不是你真正想要的数据。

iOS 中常见的锁

为了线程在访问时的安全性,我们有必要使用锁来保证。iOS 开发中,常用的锁有以下几种:

  • SLock
  • SRecursiveLock
  • SCondition
  • SConditionLock
  • thread_mutex
  • hread_rwlock
  • OSIX Conditions
  • SSpinLock
  • s_unfair_lock
  • synchronized

NSLock

NSLock 实现了最基本的互斥锁,遵循了NSLocking 协议,通过 lockunlock 方法加锁和解锁。

objc 复制代码
@interface ViewController ()
@property (strong, nonatomic) NSLock *lock;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.lock = [[NSLock alloc] init];

    NSThread *yourThread = [[NSThread alloc] initWithTarget:self selector:@selector(blindDate) object:nil];
    yourThread.name = @"你";

    NSThread *otherThead = [[NSThread alloc] initWithTarget:self selector:@selector(blindDate) object:nil];
    otherThead.name = @"别人";

    [yourThread start];
    [otherThead start];
}

- (void)blindDate {
    [self.lock lock];
    NSLog(@"%@正在相亲", [NSThread currentThread].name);
    sleep(2);
    NSLog(@"%@相亲结束", [NSThread currentThread].name);
    [self.lock unlock];
}

如果我们把锁撤掉:

是不是乱套了,妹纸此时此刻应该是一脸懵逼的状态。

NSRecursiveLock

归锁,该锁可以被一个线程多次获取,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获取锁,就必须有与其对应的释放锁,保证不会出现死锁。并且只有所有的锁被释放之后,其他线程才可以继续获得锁。

objc 复制代码
self.lock = [[NSRecursiveLock alloc] init];

NSThread *interviewThread1 = [[NSThread alloc] initWithTarget:self selector:@selector(interview) object:nil];
interviewThread1.name = @"面试官 Jack";
[interviewThread1 start];

NSThread *interviewThread2 = [[NSThread alloc] initWithTarget:self selector:@selector(interview) object:nil];
interviewThread2.name = @"面试官 Rose";
[interviewThread2 start];

- (void)interview {

for (int i = 1; i < 4; i++) {
    [self.lock lock];
    NSLog(@"候选人%d正在面试,%@忙碌", i, [NSThread currentThread].name);
    sleep(2);
    NSLog(@"候选人%d面试结束,%@空闲", i, [NSThread currentThread].name);
    [self.lock unlock];
}
}

从输出结果看到,加锁之前,两个面试官同时面试一个候选人。而在加锁之后,面试场景变为初试和复试,先由一个面试官 Jack 负责初试,由面试官 Rose 负责复试。这个面试的场景可能放在这里欠妥,我们主要了解递归锁的使用方法。

NSCondition

NSCondition 是一种特殊的锁,它可以实现不同线程间的调度。线程 1 因不满足某一个条件而遭到阻塞,直到线程 2 满足该条件从而发出放行信号给线程 1,此后,线程 1 才可以被正确执行。

下面我们模拟图片下载到处理的过程,开辟一个线程远程网络下载图片,开辟另一个线程处理下载好的图片,由于处理图片的线程因没有图片,而被限行,只有在图片下载完成后,才去处理图片。这样就可以在下载线程完成图片下载后发出一个信号,让另一个线程在拿到图片后在其线程上处理图片。

objc 复制代码
self.condition = [[NSCondition alloc] init];

NSThread *downloadImageThread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImage) object:nil];
downloadImageThread.name = @"下载图片";

NSThread *dealWithImageThread = [[NSThread alloc] initWithTarget:self selector:@selector(dealWithImage) object:nil];
dealWithImageThread.name = @"处理图片";

[downloadImageThread start];
[dealWithImageThread start];

static BOOL finished = NO;

- (void)downloadImage {
    [self.condition lock];

    NSLog(@"正在下载图片...");
    sleep(2);
    NSLog(@"图片下载完成");
    finished = YES;

    if (finished) {
        [self.condition signal];
        [self.condition unlock];
    }
}

- (void)dealWithImage {

    [self.condition lock];

    while (!finished) {
        [self.condition wait];
    }

    NSLog(@"正在处理图片...");
    sleep(2);
    NSLog(@"图片处理完成");
    [self.condition unlock];
}

NSConditionLock

NSConditionLock 互斥锁跟 NSCondition 很像,可以在某种条件下进行加锁和解锁,但实现方式不同。当两个线程需要特定顺序执行的时候,就可以使用 NSConditionLock。如生产者消费者模型,当生产者执行的时候,消费者可以通过特定的条件获得锁;当生产者完成时解锁,然后把说的条件设置成唤醒消费者线程的条件。

加锁和解锁调用可以随意组合,lockunlockWithCondition: 配合使用,lockWhenCondition:unlock 配合使用。

objc 复制代码
@interface ViewController ()
@property (strong, nonatomic) NSConditionLock *conditionLock;
@end

@implementation ViewController

- (void)viewDidLoad {

    [super viewDidLoad];

    self.conditionLock = [[NSConditionLock alloc] init];

    NSThread *producerThread = [[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil];
    producerThread.name = @"生产者";

    NSThread *consumerThread = [[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil];
    consumerThread.name = @"消费者";

    [producerThread start];
    [consumerThread start];

}

- (void)producer {
    [self.conditionLock lock];
    NSLog(@"生产商品中...");
    sleep(2);
    NSLog(@"商品生产完成");
    [self.conditionLock unlockWithCondition:2];
}

- (void)consumer {
    [self.conditionLock lockWhenCondition:2];
    sleep(2);
    NSLog(@"消费者使用商品");
    [self.conditionLock unlock];
}

当生产者释放锁时,将条件设置为2,消费者就可以通过此条件获得锁,进而程序执行。如果生产者和消费者给出的条件不一致,会导致程序不能正常执行。

pthread_mutex

POSIX 互斥锁,在使用时,只需初始化一个 pthread_mutex_t,利用 pthread_mutex_lockpthread_mutex_unlock 来加锁解锁,使用完成后,需要使用 pthread_mutex_destroy 来销毁锁。

objc 复制代码
#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@end

@implementation ViewController {
    pthread_mutex_t mutex_lock;
}

- (void)viewDidLoad {

    [super viewDidLoad];

    pthread_mutex_init(&mutex_lock,NULL);
    pthread_mutex_lock(&mutex_lock);
    // .....
    pthread_mutex_unlock(&mutex_lock);
    pthread_mutex_destroy(&mutex_lock);
}

pthread_rwlock

读写锁,当我们对文件数据进行读写操作时,写操作是排他的。一旦有多个线程对同一文件数据进行写操作,那后果不可预期。多个线程对同一文件读取时是可行的。

当读写锁被一个线程以读的模式占用时,写操作的其他线程就会被阻塞,但读操作的其他线程可以继续工作。 当读写锁被一个线程以写的模式占用时,写操作的其他线程会被阻塞,读操作的其他线程也被堵塞。

objc 复制代码
#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@end

@implementation ViewController {
    pthread_rwlock_t rwlock;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;
    rwlock = lock;

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readDataWithFlag:1];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readDataWithFlag:2];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self writeDataWithFlag:3];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self writeDataWithFlag:4];
    });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self readDataWithFlag:5];
    });
}

- (void)readDataWithFlag:(NSInteger)flag {
    pthread_rwlock_rdlock(&rwlock);
    NSLog(@"开始读取文件数据 -- %ld", flag);
    sleep(2);
    NSLog(@"读取完成 -- %ld", flag);
    pthread_rwlock_unlock(&rwlock);
}

- (void)writeDataWithFlag:(NSInteger)flag {
    pthread_rwlock_wrlock(&rwlock);
    NSLog(@"开始写入文件数据 -- %ld", flag);
    sleep(2);
    NSLog(@"写入完成 -- %ld", flag);
    pthread_rwlock_unlock(&rwlock);
}

POSIX Conditions

POSIX 条件锁 = 互斥锁 + 条件。初始化条件和互斥锁,当 ready_to_go 为 flase 的时候,进入循环,然后线程将会挂起,直到另一个线程将 ready_to_go 设置为 ture 的时候,并且发送信号的时候,该线程才会被唤醒。

objc 复制代码
#import "ViewController.h"
#import <pthread.h>

@interface ViewController ()
@end

@implementation ViewController {
    pthread_mutex_t mutex;
    pthread_cond_t condition;
    Boolean ready_to_go;
}

- (void)condInit {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&condition, NULL);
    ready_to_go = true;
}

- (void)waitWhenConditionCompleted {
    pthread_mutex_lock(&mutex);

    while (ready_to_go == false) {
        pthread_cond_wait(&condition, &mutex);
    }

    // insert your code ...

    ready_to_go = false;

    pthread_mutex_unlock(&mutex);
}

- (void)signalThreadUsingCondition {
    pthread_mutex_lock(&mutex);
    ready_to_go = true;

    pthread_cond_signal(&condition);

    pthread_mutex_unlock(&mutex);
}

OSSpinLock

自旋锁,与互斥锁类似。但二者还是有区别的:

  1. 互斥锁,当一个线程获得这个锁后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。
  2. 自旋锁,当一个线程获得这个锁后,其他线程将会一直循环检测,可该锁时候被解锁。

因此,自旋锁适用于持有者在较短的情况下持有该锁。

objc 复制代码
#import "ViewController.h"
#import <libkern/OSAtomic.h>

@interface ViewController ()
@end

@implementation ViewController {
    OSSpinLock spinLock;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    spinLock = OS_SPINLOCK_INIT;

    OSSpinLockLock(&spinLock);

    OSSpinLockUnlock(&spinLock);
}

当然,YYKit 作者也有提到过 不再安全的 OSSpinLock,这个自旋锁存在优先级反转的问题。因此这种锁也不再安全。在 iOS 10.0 后,Apple 也将其弃用了。

os_unfair_lock

OSSpinLock 因存在优先级反转的问题而使得其不再安全,为此 Apple 推出了 os_unfair_lock_t,用于解决反转的问题。

objc 复制代码
#import "ViewController.h"
#import <os/lock.h>

@interface ViewController ()
@end

@implementation ViewController {

}

- (void)viewDidLoad {
    [super viewDidLoad];

    os_unfair_lock_t unfair_lock;

    unfair_lock = &(OS_UNFAIR_LOCK_INIT);
    os_unfair_lock_lock(unfair_lock);

    // insert your code ...

    os_unfair_lock_unlock(unfair_lock);
}

dispatch_semaphore

利用信号量的机制实现锁的功能,同样也是等待信号和发送信号的过程。当多个线程访问时,只要有一个获得信号,那么其他线程就必须等待该信号释放。

objc 复制代码
#import "ViewController.h"

@interface ViewController ()
@end

@implementation ViewController {
    dispatch_semaphore_t semaphore_t;
}

- (void)viewDidLoad {

    [super viewDidLoad];

    dispatch_semaphore_wait(semaphore_t, DISPATCH_TIME_FOREVER);

    // insert your code ...

    dispatch_semaphore_signal(semaphore_t);
}

@synchronized

便捷的互斥锁。如果你在多个线程中传过去的是同一个标识符,那么先获得锁的会锁定其中的代码块,其他线程将会被阻塞,反之亦然。但使用这种锁,程序效率比较低,所以我们更多的会使用其他方式的锁来代替。

objc 复制代码
@synchronized (self) {
    // insert your code ...
}

总结

  1. 如果进行文件读写操作,使用 pthread_rwlock 比较好。因为文件读写通常会消耗大量资源,如果使用互斥锁,在多个线程同时读取文件的时候,会阻塞其他读文件的线程,而 pthread_rwlock 则不会这样,所以使用其进行文件的读写,会更合适一点。
  2. 如果用到互斥锁,当性能要求比较高,可使用 pthread_mutex 或者 dispath_semaphore

参考

# iOS中的各种锁

相关推荐
名字不要太长 像我这样就好7 天前
【iOS】源码阅读(二)——NSObject的alloc源码
开发语言·macos·ios·objective-c
小鹿撞出了脑震荡7 天前
「OC」源码学习—— 消息发送、动态方法解析和消息转发
学习·ios·objective-c
小鹿撞出了脑震荡10 天前
「OC」源码学习——objc_class的bits成员探究
学习·ios·objective-c
画个大饼10 天前
iOS启动优化:从原理到实践
macos·ios·objective-c·swift·启动优化
他们都不看好你,偏偏你最不争气11 天前
OC语言学习——面向对象(下)
开发语言·学习·objective-c·面向对象
画个大饼19 天前
Objective-C Block 底层原理深度解析
开发语言·ios·objective-c
MrZWCui19 天前
iOS—仿tableView自定义闹钟列表
学习·macos·ios·objective-c
名字不要太长 像我这样就好19 天前
【iOS】OC源码阅读——alloc源码分析
笔记·学习·macos·ios·objective-c
zhishishe19 天前
如何修复卡在恢复模式下的 iPhone:简短指南
windows·macos·ios·objective-c·cocoa·iphone
xiaonianzuibang20 天前
如何修复宝可梦时时刻刻冒险无法正常工作
macos·objective-c·cocoa