iOS 多线程(一)-锁

1.概述
1.1 类型
  • 自旋锁 OSSpinLock
  • 信号量 dispatch_semaphore
  • 互斥锁 pthread_mutex、NSLock
  • 递归锁 pthread_mutex(recursive)、NSRecursiveLock、@sychronized
  • 条件锁 NSCondition、NSConditionLock

参考一张著名的图:


image

他们的性能从上到下依次降低

1.2 基础原理
  • 什么是锁?通过一种机制,将代码片段划分成一段一段临界区域,确保同一时间只有一个临界区域处于运行状态;锁主要解决线程同步问题,避免引发数据混乱等未知问题
  • 互斥锁 也叫同步锁
  • 递归锁 其实也属于互斥锁的一种,对于互斥锁,当同一个线程多次访问同一个锁时会引发死锁;而递归锁不会出现此问题,递归锁的例子,参考Reentrant_mutex#Example

    Thread A calls function F which acquires a reentrant lock for itself before proceeding
    Thread B calls function F which attempts to acquire a reentrant lock for itself but cannot due to one already outstanding, resulting in either a block (it waits), or a timeout if requested
    Thread A's F calls itself recursively. It already owns the lock, so it will not block itself (no deadlock). This is the central idea of a reentrant mutex, and is what makes it different from a regular lock.
    Thread B's F is still waiting, or has caught the timeout and worked around it
    Thread A's F finishes and releases its lock(s)
    Thread B's F can now acquire a reentrant lock and proceed if it was still waiting

理解:当一个线程调用F加锁之后,若F中又调用自己,就不再加锁了,所以不会死锁

  • 自旋锁 是一种内核级别的锁, 采用忙等机制加锁,OSSpinLock的头文件是<libkern/OSAtomic.h>,该锁在iOS10以上废弃了,改为os_unfair_lock_t
2. 实现原理

参考:探讨iOS开发中的各种锁

2.1 原理概述
  • 这么多种锁其实大的实现思路就两种,一种叫忙等待,一种叫阻塞
  • 所谓忙等待就是,让一个线程处于一直处于等待状态,直至锁释放;阻塞是让一个线程挂起,直至锁释放,操作系统会切换上下文到另个线程;相比而言,忙等待机制会更耗CPU资源,因为线程还在跑

现在互斥锁大多使用队列和上下文切换以达到节约资源和降低延迟的目的;但总有情况,挂起一个线程,然后切换上下文再恢复的时间比线程忙等所用的时间长,所以这时候就要用自旋锁。参考:互斥锁

  • 使用锁会出现优先级倒置(高优先级线程会等待地优先级线程执行,下边OSSpinLock自旋锁就是,有详细解释)、资源饥荒(某个线程一直得不到足够的锁资源)的情况
2.2 各种锁的原理
  • OSSpinLock 是一种忙等机制,利用全局变量表示锁的状态,它会存在优先级反转问题,就是说当高优先级通过信号机制访问低优先级,信号量已经被低优先级占有,导致高优先级任务被低优先级任务所阻塞
  • dispatch_semaphore 信号量机制不仅能操作线程,其实还能够操作进程,只是iOS 单个应用来说是单进程的(此句有误,UI渲染就在一个新线程叫backbond);当wait时,信号量的值-1;当signal时,信号量的值+1,当信号量的值==0时,当前线程会被阻塞;所以初始值应该传入1
  • pthread_mutex 互斥锁,是一套跨平台的API,采用的阻塞机制
  • pthread_mutex(recursive) 与pthread_mutex类似
  • NSLock 一种互斥锁,内部封装的 pthread_mutex实现
  • NSRecursiveLock 与NSLock类似,内部封装的pthread_mutex实现,锁的类型不同PTHREAD_MUTEX_RECURSIVE
  • NSCondition 封装了一个互斥锁和条件变量
  • NSConditionLock 以此来实现(以上两个没有用过,没有发言权暂不探究了)
  • @sychronized 这个性能最慢的锁,但用起来也最爽,它的原理是需要深入了解的(凡是封装的较深的是有必要探究下的)

NSLock是pthread_mutex的封装可以通用
NSRecursiveLock是pthread_mutex(recursive) 的封装

2.3 @sychronized原理

参考:关于 @synchronized,这儿比你想知道的还要多
具体探究过程请参考文章,结合源码阅读即可
一句话总结:

编译器会将{}代码放入try{}finally{}中,在{开始时,会转化成objc_sync_enter方法,内部调用pthread_mutex创建一个递归锁,并分配给@sychronized(obj)传入的obj;在}结束时调用objc_sync_exit方法释放分配的锁

伪代码如下:

NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
    objc_sync_enter(synchronizeTarget);
    test = nil;
} @finally {
    objc_sync_exit(synchronizeTarget);   
}

理解:

  1. 分配的锁通过对象的内存地址与对象建立关联
  2. 若传入nil对象,不会出问题,但等于没有加锁
  3. 若在{}中对象置为nil,也不会出现问题,编译器做了处理,即保证enter和exit方法传入的对象是同一个
  4. @sychronized不会修改对象的引用计数
3. 使用场景

目前用过两种,一种是互斥锁,一种是信号量,平时开发中把这两个掌握好基本就能满足大部分需求,从一些知名的开源库中寻找使用场景:
3.1 在ReactiveCocoa 中大量使用OSSpinLock,来保证大量全局变量的同步,一般这样用
eg: 在RACCompoundDisposable.m中

- (BOOL)isDisposed {
    OSSpinLockLock(&_spinLock);
    BOOL disposed = _disposed;
    OSSpinLockUnlock(&_spinLock);

    return disposed;
}

3.2 在AFNetworking、ReactiveCocoa中可以看到NSLock
eg:在AFURLResponseSerializer.m中,可以看到从NSData转UIImage时使用了同步锁,解决[UIImage imageWithData:data]线程不安全的问题,参考issue #2572 Crash on AFImageWithDataAtScale when loading image

+ (UIImage *)af_safeImageWithData:(NSData *)data {
    UIImage* image = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        imageLock = [[NSLock alloc] init];
    });
    
    [imageLock lock];
    image = [UIImage imageWithData:data]; //该方法不是线程安全的
    [imageLock unlock];
    return image;
}

3.3 在CocoaLumberjack中大量使用pthread_mutex来保证线程同步,在DDContextFilterLogFormatter.m一个例子:

- (NSArray *)currentSet {
    NSArray *result = nil;

    pthread_mutex_lock(&_mutex);
    {
        result = [_set allObjects];
    }
    pthread_mutex_unlock(&_mutex);

    return result;
}

3.4 在AFURLSessionManager.m tasksForKeyPath方法使用dispatch_semaphore_t来阻塞当前线程,例子如下:

- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //当getTasksWithCompletionHandler执行完毕signal时时,才会该函数才会return,是一种保证函数返回值同步的好办法

    return tasks;
}

小总结:

  1. 使用最多的是NSLock这种形式、其次是pthread_mutex和信号量
  2. 尽量不要使用@sychronized这种方式,因为它性能较低
4. 补充

多线程中栈是私有的,堆是公有的
每个线程拥有一个栈和计数器,栈和计数器用来保存线程的执行历史和执行状态;
其他资源(堆、地址空间、全局变量)是同一个进程内多个线程共享

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 3,971评论 0 0
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 4,204评论 0 2
  • 多线程需要一种互斥的机制来访问共享资源。 一、 互斥锁 互斥锁的意思是某一时刻只允许一个线程访问某一资源。为了保证...
    doudo阅读 4,106评论 0 5
  • 前言 iOS开发中由于各种第三方库的高度封装,对锁的使用很少,刚好之前面试中被问到的关于并发编程锁的问题,都是一知...
    喵渣渣阅读 9,147评论 0 33
  • iOS线程安全的锁与性能对比 一、锁的基本使用方法 1.1、@synchronized 这是我们最熟悉的枷锁方式,...
    Jacky_Yang阅读 6,776评论 0 17

友情链接更多精彩内容