iOS 开发常见的几种锁

简介

在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题。我们常常会使用一些锁来保证程序的线程安全,保证每次只有一个线程访问这一块资源。

多线程编程中,应该尽量避免资源在线程之间共享,以减少线程间的相互作用。

锁的分类

锁住要分为以下几类:

互斥锁

它将代码切片成为一个个代码块,使得当一个代码块在运行时,其他线程不能运行他们之中的任意片段,只有等到该片段结束运行后才可以运行。通过这种方式来防止多个线程同时对某一资源进行读写的一种机制。常用的有:

  • @synchronized

  • NSLock

  • pthread_mutex

自旋锁

多线程同步的一种机制,当其检测到资源不可用时,会保持一种“忙等”的状态,直到获取该资源。它的优势在于避免了上下文的切换,非常适合于堵塞时间很短的场合;缺点则是在“忙等”的状态下会不停的检测状态,会占用 cpu 资源。常用的有:

  • OSSpinLock

  • atomic

条件锁

通过一些条件来控制资源的访问,当然条件是会发生变化的。常用的有:

  • NSCondition

  • NSConditionLock

信号量

是一种高级的同步机制。互斥锁可以认为是信号量取值0/1时的特例,可以实现更加复杂的同步。常用的有:

  • dispatch_semaphore

递归锁

它允许同一线程多次加锁,而不会造成死锁。递归锁是特殊的互斥锁,主要是用在循环或递归操作中。常用的有:

  • pthread_mutex(recursive)

  • NSRecursiveLock

读写锁

是并发控制的一种同步机制,也称“共享-互斥锁”,也是一种特殊的自旋锁。它把对资源的访问者分为读者和写者,它允许同时有多个读者访问资源,但是只允许有一个写者来访问资源。常用的有:

  • pthread(rwlock)

  • dispatch_barrier_async / dispatch_barrier_sync

常见几种锁的使用方法

OSSpinLock(iOS 10 以后废弃)

它是一种自旋锁,只有加锁,解锁,尝试加锁三个方法,其中尝试加锁是非线程阻塞的。通过导入 #import 引入并调用,使用示例:

    OSSpinLock lock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&lock);
    //执行代码
    OSSpinLockUnlock(&lock);

OSSpinLock 有可能会造成死锁,不再安全的锁:

有可能在优先级比较低的线程里对共享资源加锁了,然后高优先级的线程抢占了低优先级的调用 cpu 时间,导致高优先级的线程一直在等待低优先级的线程释放锁,然而低优先级的线程根本没法抢占高优先级的 cpu 时间。(优先级反转)

NSLock

是一种互斥锁,在 Cocoa 程序下 所有锁(包括 NSLockNSConditionNSRecursiveLockNSConditionLock) 的接口实际上是通过 NSLocking 协议定义的,它定义了 lockunlock 方法,使用这些方法来获取和释放该锁。

@protocol NSLocking

- (void)lock;
- (void)unlock;

@end

此外,NSLock 类还添加了如下方法:

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
  • tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程,相反,它只是返回 NO
  • lockBeforeDate: 方法试图获取一个锁,但是如果锁没有在规定的时间内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回NO)

NSRecursiveLock

是一个递归锁。NSRecursiveLock 类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它被多少次 lock。每次成功的 lock 都必须平衡调用unlock 操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。在使用锁时最容易犯的一个错误就是在递归或循环中造成死锁,如下代码:

//创建锁
NSLock *lock = [[NSLock alloc] init];

//线程1
dispatch_async(dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT), ^{
    static void(^TestBlock)(int);
    TestBlock = ^(int value) {
        NSLog(@"加锁: %d",value);
        [lock lock];
        if (value > 0) {
            TestBlock(--value);
        }
        NSLog(@"程序退出!");
        [lock unlock];
    };
    
    TestBlock(5);
});

在线程1中的递归 block 中,锁会被多次的 lock,所以自己也被阻塞了。此处将NSLock 换成 NSRecursiveLock,便可解决问题。

NSRecursiveLock *rslock = [[NSRecursiveLock alloc] init];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void(^TestBlock)(int);
    TestBlock = ^(int value) {
        NSLog(@"加锁: %d",value);
        [rslock lock];
        if (value > 0) {
            TestBlock(--value);
        }
        
        NSLog(@"程序退出!");
        [rslock unlock];
    };

    TestBlock(5);
});

NSCondition

NSCondition 的对象实际上是作为一个锁和线程检查器,锁主要是为了检测条件时保护数据源,执行条件引发的任务。线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。

NSCondition 除了 lockunlock 方法来使用解决线程同步问题,还提供了更高级的用法:

- (void)wait; //让当前线程处于等待状态
- (BOOL)waitUntilDate:(NSDate *)limit; //让当前线程处于等待到什么时间
- (void)signal; // CPU发信号告诉正在等待中的线程不用在等待,可以继续执行(只对一个线程起作用)
- (void)broadcast;// 通知所有在等在等待中的线程(广播)

@property (nullable, copy) NSString *name;
  • wait 堵塞当前线程,使线程进入休眠,等待唤醒信号。
  • waitUntilDate 堵塞当前线程,线程进入休眠,等待唤醒信号或者超时。如果是被信号唤醒返回 YES,否者返回 NO
  • signal 唤醒一个正在休眠的线程,如果要唤醒多个线程,需要调用多次,如果没有线程在等待,什么也不做。
  • broadcast 唤醒所有在等待的线程,如果没有线程在等待,什么也不做。

以上的方法在调用前必须已经加锁(lock)

NSCondition 是条件,条件是我们自己决定的。和 NSLock@synchronized 等是不同的是,NSCondition 可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。代码示例:

NSCondition *condition =[[NSCondition alloc] init];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [condition lock];
    NSLog(@"线程1加锁");
    while ([self.testArray count] == 0) {
        NSLog(@"waiting...");
        [condition wait];
    }
    [self.testArray removeObjectAtIndex:0];
    NSLog(@"delete one object");
    NSLog(@"线程1退出");
    [condition unlock];
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [condition lock];
    NSLog(@"线程2加锁");
    self.testArray = [NSMutableArray array];
    [self.testArray addObject:[[NSObject alloc] init]];
    NSLog(@"add one object");
    [condition signal];
    NSLog(@"线程2退出");
    [condition unlock];
});

condition 进入到判断条件中,当 self.testArray count == 0 时,condition 会调用 wait,当前线程处于等待状态,其他线程开始访问 self.testArray,当对象创建完毕并加入 self.testArray 中时,cpu 会发出 signal 信号,处于等待的线程就会被唤醒,开始执行 [self.testArray removeObjectAtIndex:0];

打印结果如下:

NSConditionLock

NSConditionLock 为条件锁,只有 condition 参数与初始化时候的 condition 相等,才能进行加锁操作。而 unlockWithCondition: 并不是当 condition 符合条件时才解锁,而是解锁之后,修改 condition 的值。提供的方法如下:

- (instancetype)initWithCondition:(NSInteger)condition; //初始化对象。有一个整形的conditon参数,表示条件
- (void)lockWhenCondition:(NSInteger)condition; //进程会一直阻塞,一直到满足conditon并完成加锁
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition; //解锁并重新设定condition
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

我们来看个示例:

NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:3];

//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [lock lockWhenCondition:1];
   NSLog(@"线程1开始执行");
   [lock unlockWithCondition:0];
});

//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [lock lockWhenCondition:2];
   NSLog(@"线程2开始执行");
   [lock unlockWithCondition:1];
});

//线程3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [lock lock];
   NSLog(@"线程3开始执行");
   [lock unlock];
});

//线程4
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [lock lockWhenCondition:3];
   NSLog(@"线程4开始执行");
   [lock unlockWithCondition:2];
});

//线程5
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [lock lock];
   NSLog(@"线程5开始执行");
   [lock unlock];
});

分析:

  • 线程1,2调用 [NSConditionLock lockWhenCondition:],此时因为不满足当前条件,所以会进入等待状态。

  • 线程3,5调用 [NSConditionLock lock:],不需要比对条件值,按照 cpu 执行顺序执行,

  • 线程4执行 [NSConditionLock lockWhenCondition:],因为满足条件值,所以线程4会按照 cpu 执行顺序执行。

  • 线程4打印完成后会调用 [NSConditionLock unlockWithCondition:],这个时候将条件设置为2,并发送 boradcast,此时线程2接收到当前的信号,唤醒执行并打印;之后会执行线程1。

[NSConditionLock lockWhenCondition:] 这里会根据传入的 condition 值和 value 值进行对比,如果不相等,这里就会阻塞。而 [NSConditionLock unlockWithCondition:] 会先更改当前的 value 值,然后调用 boradcast,唤醒当前的线程。综上所述,上面的打印结果不是一定的,421 的顺序是一定的,而 3,5 是在任意位置(即只要是按照421的结果顺序都是正确的)

NSCondition & NSConditionLock 比较

相同点:

  • 都是互斥锁

  • 通过条件变量来控制加锁、释放锁,从而达到阻塞线程、唤醒线程的目的

不同点:

  • NSCondition 是基于对 pthread_mutex 的封装,而 NSConditionLock 是对 NSCondition 做了一层封装

  • NSCondition 需要手动让线程进入等待状态阻塞线程、释放信号唤醒线程,NSConditionLock 只需要外部传入一个值,就会依据这个值进行自动判断是阻塞线程还是唤醒线程

@synchronized

是一个 OC 层面的互斥锁,主要是通过牺牲性能换来语法上的简洁与可读。(性能较差不推荐使用)

@synchronized 后面需要紧跟一个 OC 对象,它实际上是把这个对象当做锁的唯一标识。这是通过一个哈希表来记录表示,OC 在底层使用了一个互斥锁的数组(你可以理解为锁池),通过对对象去哈希值在数组中得到对应的互斥锁。示例如下:

//总票数
_tickets = 5;

//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self saleTickets];
});

//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self saleTickets];
});

- (void)saleTickets {
    while (1) {
        @synchronized (self) {
            [NSThread sleepForTimeInterval:1];
            if (_tickets > 0) {
                _tickets--;
                NSLog(@"剩余票数:%ld, Thread:%@", _tickets, [NSThread currentThread]);
            }
            else {
                NSLog(@"票卖完了  Thread:%@", [NSThread currentThread]);
                break;
            }
        }
    }
}

注意点:

  • 1.加锁的代码尽量少
  • 2.添加的OC对象必须在多个线程中都是同一对象
  • 3.优点是不需要显式的创建锁对象,便可以实现锁的机制。
    1. @synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。

dispatch_semaphore

dispatch_semaphoreGCD 使用信号量控制并发。

信号量:就是一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。

在日常开发中利用 GCD 的信号量机制来处理一些日常功能的时候,主要会用到的方法有三个:

//创建信号量,会根据传入的参数创建对应数目的信号量
dispatch_semaphore_create(intptr_t value);;

//等待信号量,减少信号量计数
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

// 发送信号量,增加信号量计数
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
  • dispatch_semaphore_create 创建信号量,并且创建的时候需要指定信号量的大小

  • dispatch_semaphore_wait 等待信号量,如果信号量为0,那么该函数就会一直等待(不返回,阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。

  • dispatch_semaphore_signal 发送信号量。该函数会对信号量的值进行加1操作

等待信号量和发送信号量的函数是成对出现的。下面来看个经典的示例(异步函数+并发队列实现同步操作):

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

for (int i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务%d:%@", i + 1, [NSThread currentThread]);
        // 发送信号量
        dispatch_semaphore_signal(semaphore);
    });

    // 等待信号量
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"任务1000:%@",[NSThread currentThread]);
});

打印结果太长,截取一部分如下:

可以看到:虽然任务是一个接一个被同步(说同步并不准确)执行的,但因为是在并发队列,并不是所有的任务都是在同一个线程执行的(所以说同步并不准确)。有别于异步函数+串行队列的方式(异步函数+ 串行队列的方式中,所有的任务都是在同一个新线程被串行执行的)。

同步和异步决定了是否开启新线程(或者说是否具有开启新线程的能力),串行和并发决定了任务的执行方式——串行执行还是并发执行(或者说开启多少条新线程)

pthread

pthread,可以创建互斥锁、递归锁、读写锁、once等锁

互斥锁

不会忙等,而是阻塞线程并睡眠,需要进行上下文切换。

__block pthread_mutex_t mutex;
pthread_mutex_init(&mutex, PTHREAD_MUTEX_NORMAL);
/**
 PTHREAD_MUTEX_NORMAL 缺省类型,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后先进先出原则获得锁。
 PTHREAD_MUTEX_ERRORCHECK 检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与普通锁类型动作相同。这样就保证当不允许多次加锁时不会出现嵌套情况下的死锁。
 PTHREAD_MUTEX_RECURSIVE 递归锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。
 PTHREAD_MUTEX_DEFAULT 适应锁,动作最简单的锁类型,仅等待解锁后重新竞争,没有等待队列。
 */

//线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"线程1加锁");
    pthread_mutex_lock(&mutex);
    sleep(2);
    NSLog(@"线程1");
    pthread_mutex_unlock(&mutex);
    NSLog(@"线程1解锁");
});

//线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"线程2加锁");
    pthread_mutex_lock(&mutex);
    sleep(2);
    NSLog(@"线程2");
    pthread_mutex_unlock(&mutex);
    NSLog(@"线程2解锁");
});
递归锁
__block pthread_mutex_t recursiveMutex;
pthread_mutexattr_t recursiveMutexattr;

pthread_mutexattr_init(&recursiveMutexattr);
pthread_mutexattr_settype(&recursiveMutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&recursiveMutex, &recursiveMutexattr);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    static void (^RecursiveBlock)(int);

    RecursiveBlock = ^(int value) {
        NSLog(@"线程加锁");
        pthread_mutex_lock(&recursiveMutex);
        if (value > 0) {
            NSLog(@"value: %d",value);
            RecursiveBlock(--value);
        }
        NSLog(@"线程解锁");
        pthread_mutex_unlock(&recursiveMutex);
    };

    RecursiveBlock(3);
});
读写锁
typedef void(^ReadWriteBlock)(NSString *str);
typedef void(^VoidBlock)(void);

__block pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
__block NSMutableArray *arrayM = [NSMutableArray array];

ReadWriteBlock writeBlock = ^ (NSString *str) {
    NSLog(@"开启写操作");
    pthread_rwlock_wrlock(&rwlock);
    [arrayM addObject:str];
    sleep(2);
    pthread_rwlock_unlock(&rwlock);
};

VoidBlock readBlock = ^  {
    NSLog(@"开启读操作");
    pthread_rwlock_rdlock(&rwlock);
    sleep(1);
    NSLog(@"读取数据:%@",arrayM);
    pthread_rwlock_unlock(&rwlock);
};

for (int i = 0; i < 5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        writeBlock([NSString stringWithFormat:@"%d",I]);
    });
}

for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        readBlock();
    });
}

dispatch_barrier_async / dispatch_barrier_sync

现在有一个需求:任务1,2,3 均执行完毕执行任务 0,然后执行任务4,5,6,我们通过 GCD 的 barrier 方法来实现,如下:

- (void)dispatch_barrierTest {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lc.brrier", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务1 -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务2 -- %@", [NSThread currentThread]);
    });
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务3 -- %@", [NSThread currentThread]);
    });
    
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"任务0 -- %@", [NSThread currentThread]);
        sleep(5); //默认耗时
    });
    
    NSLog(@"dispatch_barrier 测试");
    
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务4 -- %@", [NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务5 -- %@", [NSThread currentThread]);
    });
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务6 -- %@", [NSThread currentThread]);
    });
}

运行,输出结果如下:

可以看到,任务0执行是在任务1,2,3 都执行完之后才会执行,而任务4,5,6是在任务0执行后才会执行(其中1,2,3 是不分先后顺序,同样的4,5,6也不分先后顺序)

dispatch_barrier_async 和 dispatch_barrier_sync 的区别

相同点

  • 等待前面的任务都执行完毕才会执行当前的任务

  • 当前任务执行完毕才会执行后面的任务

不同点

  • dispatch_barrier_async 将当前任务添加到队列之后,会将后续的任务也添加到队列中,但是后面的任务只能等待当前任务执行完毕,才会执行后面的任务

  • dispatch_barrier_sync 将当前任务添加到队列之后,等待当前任务执行完毕,才会将后续的任务添加到队列,然后执行任务

将上述代码中的 dispatch_barrier_sync 换成 dispatch_barrier_async 后,输出结果为:

拓展

property - atomic & nonatomic

atomic 修饰的对象,系统会保证在其自动生成的 getter/setter 方法中的操作是完整的,不受其他线程的影响。

atomic
  • 默认修饰符
  • 会保证CPU能在别的线程访问这个属性之前先执行完当前操作
  • 读写速度慢
  • 线程不安全 - 如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
nonatomic
  • 手动写
  • 速度更快
  • 线程不安全
  • 如果两个线程同时访问会出现不可预料的结果

单例实现

单例:该类在程序运行期间有且仅有一个实例

使用 GCD 来实现
- (id)shareInstance {
    static id shareInstance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!shareInstance) {
            shareInstance = [[NSObject alloc] init];
        }
    });
    return shareInstance;
}
通过 pthread 来实现
- (void)lock {
    pthread_once_t once = PTHREAD_ONCE_INIT;
    pthread_once(&once, onceFunc);
}

void onceFunc() {
    static id shareInstance;
    shareInstance = [[NSObject alloc] init];
}

你可能感兴趣的:(iOS 开发常见的几种锁)