iOS多线程-锁

366 阅读6分钟

前面几篇我们探索了iOS使用频率很高的多线程技术GCD,本篇我们探索多线程中一个重要的概念

锁的分类

锁主要分为两大类自旋锁互斥锁

自旋锁

在自旋锁中,线程会反复检查变量是否可用。由于线程这个过程中一致保持执行,所以是一种忙等待。 一旦获取了自旋锁,线程就会一直保持该锁,直到显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合有效的。对于iOS属性的修饰符atomic,自带一把自旋锁

互斥锁

互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(例如全局变量)进行读写的机制,该目的是通过将代码切成一个个临界区而达成。

读写锁

读写锁实际是一种特殊的互斥锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU 数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞.当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁.通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞.读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。

几种锁的性能对比

我们通过代码打印的方式比较各种锁性能:

    int js_runTimes = 100000;
    /** OSSpinLock 性能 */
    {
        OSSpinLock js_spinlock = OS_SPINLOCK_INIT;
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            OSSpinLockLock(&js_spinlock);          //解锁
            OSSpinLockUnlock(&js_spinlock);
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"OSSpinLock: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
    /** dispatch_semaphore_t 性能 */
    {
        dispatch_semaphore_t js_sem = dispatch_semaphore_create(1);
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            dispatch_semaphore_wait(js_sem, DISPATCH_TIME_FOREVER);
            dispatch_semaphore_signal(js_sem);
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"dispatch_semaphore_t: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
    /** os_unfair_lock_lock 性能 */
    {
        os_unfair_lock js_unfairlock = OS_UNFAIR_LOCK_INIT;
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            os_unfair_lock_lock(&js_unfairlock);
            os_unfair_lock_unlock(&js_unfairlock);
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"os_unfair_lock_lock: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
    
    /** pthread_mutex_t 性能 */
    {
        pthread_mutex_t js_metext = PTHREAD_MUTEX_INITIALIZER;
      
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            pthread_mutex_lock(&js_metext);
            pthread_mutex_unlock(&js_metext);
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"pthread_mutex_t: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
    
    /** NSlock 性能 */
    {
        NSLock *js_lock = [NSLock new];
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            [js_lock lock];
            [js_lock unlock];
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"NSlock: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
    /** NSCondition 性能 */
    {
        NSCondition *js_condition = [NSCondition new];
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            [js_condition lock];
            [js_condition unlock];
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"NSCondition: %f ms",(js_endTime - js_beginTime)*1000);
    }
​
    /** PTHREAD_MUTEX_RECURSIVE 性能 */
    {
        pthread_mutex_t js_metext_recurive;
        pthread_mutexattr_t attr;
        pthread_mutexattr_init (&attr);
        pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init (&js_metext_recurive, &attr);
        
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            pthread_mutex_lock(&js_metext_recurive);
            pthread_mutex_unlock(&js_metext_recurive);
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"PTHREAD_MUTEX_RECURSIVE: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
    /** NSRecursiveLock 性能 */
    {
        NSRecursiveLock *js_recursiveLock = [NSRecursiveLock new];
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            [js_recursiveLock lock];
            [js_recursiveLock unlock];
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"NSRecursiveLock: %f ms",(js_endTime - js_beginTime)*1000);
    }
    
​
    /** NSConditionLock 性能 */
    {
        NSConditionLock *js_conditionLock = [NSConditionLock new];
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            [js_conditionLock lock];
            [js_conditionLock unlock];
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"NSConditionLock: %f ms",(js_endTime - js_beginTime)*1000);
    }
​
    /** @synchronized 性能 */
    {
        double_t js_beginTime = CFAbsoluteTimeGetCurrent();
        for (int i=0 ; i < js_runTimes; i++) {
            @synchronized(self) {}
        }
        double_t js_endTime = CFAbsoluteTimeGetCurrent() ;
        JSLog(@"@synchronized: %f ms",(js_endTime - js_beginTime)*1000);
    }

在iPhone 12pro模拟器打印的结果为:

模拟器锁性能.jpg

在iPhone12 mini真机的结果如下:

12mini锁的性能.jpg

可以看到模拟器上 @synchronized锁性能是比较差的,但12系列(xr经过测试并没提高)手机的性能有很大提升,我们项目中会比较常见,我们就从 @synchronized开始探索。

@synchronized

我们在main.m文件里写一个锁:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
        @synchronized (appDelegateClassName) {
       
        }
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

使用xcrun命令将其编译成.cpp文件

xcrun -sdk iphoneos clang -arch arm64e -rewrite-objc main.mmain.cpp文件最下方找到main函数的实习,定位到@synchronized代码块

xcrunsyn.jpg

将代码排版之后:

        { 
          id _rethrow = 0; 
          id _sync_obj = (id)appDelegateClassName; 
          objc_sync_enter(_sync_obj);
          try {
            struct _SYNC_EXIT {
                 _SYNC_EXIT(id arg) : sync_exit(arg) {}
                ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
                 id sync_exit;
             } 
            _sync_exit(_sync_obj);
          } catch (id e) {_rethrow = e;}
          { 
            struct _FIN { _FIN(id reth) : rethrow(reth) {}
           ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
           id rethrow;
         } _fin_force_rethow(_rethrow);}
        }

加锁成功的情况我们只需要关注try代码块及以上的代码。经过我们的简化后的代码:

id _sync_obj = (id)appDelegateClassName; 
objc_sync_enter(_sync_obj);
objc_sync_exit(sync_exit);

可以看到主要就是执行了两个函数objc_sync_enterobjc_sync_exit

libobjc源码分析

通过打符号断点objc_sync_enter,我们可以知道objc_sync_enterlibobjc源码中。

libobjc_enter.jpg

接下来我们在libobjc源码中全局搜索objc_sync_enter:

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;
}

如果传入objnil,会什么也不做。主要看if代码块的代码,可以看到加锁是通过data->mutex.lock(),也就是SyncData的实例,所以我们先探究一下SyncData类型的结构:

SyncData
typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;
  • nextData:链表结构下一个节点
  • object:参照关联对象的结构,哈希表
  • threadCount:使用block的线程数量
  • mutex:递归锁(单用多线程会出现问题)

在初始化SyncData实例的时候使用的是id2data函数,我们接下来探索这个函数。

id2data函数

id2data函数有150+行,我们先隐藏代码块,总览一下结构:

id2data.jpg

函数的最开始两行有两个宏,它们的定义如下:

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

sDataLists是一个静态哈希表结构,我们使用lldb查看它的数据结构:

sData.jpg 经过断点调试,第一次进来执行的代码是:

    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;//头插法 添加近链表
    *listp = result;

这里有个细节就是listp使用的头插法将新的元素添加到链表,它的可递归性的实现依赖了这种数据结构的使用。

小结

  • synchronized的数据结构是哈希表,采用的拉链法处理哈希冲突

  • sDataLists arrary key是和对象相关的,拉链链表里的元素是同一个对象的锁。

  • objc_sync_enter和objc_sync_exit是对称的,它是一把递归锁。

  • 会有两种存储结构:tls和catch

  • 第一次访问syncData采用的是头插法链表结构 标记threadCount = 1

  • 后续访问,会判断是不是同一个对象,同一对象lockcount++,不是同一个对象threadCount++1。

  • synchronized是一种可重入、递归的多线程锁,原因

    • tls保障 threadCount 可以有多个线程对这个锁对象加锁
    • lock++会记录总共锁了多少次。

NSLock和NSRecursiveLock使用

我们以一个实例分析这两种锁的区别:

    for (int i= 0; i<10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^testMethod)(int);
            testMethod = ^(int value){
            if (value > 0) {
                NSLog(@"current value = %d",value);
                testMethod(value - 1);
                }
            };
            testMethod(10);
        });
    }

上面这个代码有多线程冲突的问题,打印的结果无序,不是我们想要的结果

没有锁无序.jpg

使用NSLock解决问题

使用NSLock解决的方法其实很简单,就是在我们调用方法testMethod前后加锁。

nslock.jpg

我们再看打印结果就正常了。

nslock打印结果.jpg

NSLock适用的是在最外层加锁,如果我们能写的代码只能在testMethod操作,这个时候加NSLock就不会正常工作了。

nslock加在业务代码.jpg

使用NSRecursiveLock

上面我们知道NSLocktestMethod无法解决问题,我们尝试用NSRecursiveLock解决。

nsrecrusiveLock.jpg

发现NSRecursiveLock并不能解决问题,而且还会偶现崩溃。NSRecursiveLock是一把递归锁,但是它并不支持多线程递归。

使用@synchronized

sychorsize解决问题.jpg

使用@synchronized解决了业务代码里的问题,说明@synchronized是一把支持多线程的递归锁。

NSCondition

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

  • [condition lock] :一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
  • [condition unlock];//lock 同时使用
  • [condition wait];// 让当前线程处于等待状态
  • [condition signal];//CPU发信号告诉线程不用在等待,可以继续执行

它的一个应用场景之一就是生产者-消费者模型。也就是通过多线程进行生产和销售产品,当产品数量为0的时候就只能等待,具体代码如下:

- (void)js_testConditon{
    _testCondition = [[NSCondition alloc] init];
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self js_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self js_consumer];
        });
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self js_consumer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self js_producer];
        });
    }
}
- (void)js_producer{
    [_testCondition lock]; // 操作的多线程影响
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal]; // 信号
    [_testCondition unlock];
}
- (void)js_consumer{
     [_testCondition lock];  // 操作的多线程影响
    if (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        [_testCondition wait];
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
     [_testCondition unlock];
}

Foundation源码看锁的封装

我们在swift-corelibs-foundation源码中探索。 通过源码我们看到,NSLock等锁都实现了一个协议就是NSLocking

public protocol NSLocking {
    func lock()
    func unlock()
}

它们都是对pthread的封装

锁的源码.jpg

NSRecursiveLock,也类似,它和NSLock的区别是pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))

nsrecursivelock源码.jpg

NSConditionLock

  • NSConditionLock 是锁,一旦一个线程获得锁,其他线程一定等待
  • lock函数:表示 对象期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
  • [xx lockWhenCondition:A条件]方法:表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的 完成,直至它解锁。
  • [xxx unlockWithCondition:A条件 ]; 表示释放锁,同时把内部的condition设置为A条件
  • return = [xxx lockWhenCondition:A条件 beforeDate:A时间 ]; 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO, 它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理。

栅栏函数实现读写锁

读写锁主要要实现以下功能:

  • 多读单写功能。
  • 写入和写入互斥。
  • 读和写入互斥。
  • 写入不能阻塞主线程任务执行。
@interface ViewController ()@property (nonatomic, strong) dispatch_queue_t js_currentQueue;
@property (nonatomic, strong) NSMutableDictionary *mDict;
@end@implementation ViewController
​
- (void)viewDidLoad {
    [super viewDidLoad];
    self.js_currentQueue = dispatch_queue_create("jscurrent", DISPATCH_QUEUE_CONCURRENT);
    self.mDict = [[NSMutableDictionary alloc] init];
    [self js_safeSetter:@"123" time:10];
    [self js_safeSetter:@"456" time:5];
    [self js_safeSetter:@"789" time:3];
    
}
​
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"读取,name = %@ thread---%@",[self js_safeGetter],[NSThread currentThread]);
        });
    }
}
​
- (void)js_safeSetter:(NSString *)name time:(int)time{
    dispatch_barrier_async(self.js_currentQueue, ^{
        sleep(time);
        [self.mDict setValue:name forKey:@"name"];
        NSLog(@"写入,name = %@ thread---%@",name,[NSThread currentThread]);
    });
    
}
​
- (NSString *)js_safeGetter{
    __block NSString *result;
    dispatch_sync(self.js_currentQueue, ^{
        result = self.mDict[@"name"];
    });
    return result;
}
@end

程序运行我们就开始点击屏幕(读操作),最后看打印结果:

读写锁.jpg