线程安全——锁

前言

多线程利用CPU多核的性质。能并行执行任务,提高效率。但随着多线程的使用,对于资源的静态以及数据的操作都可能存在风险,所以有必要在操作时保证线程安全。而线程的安全主要是依靠各种锁,锁的种类有很多,有各自的优缺点,需要开发者自己权衡利弊,选择合适的锁,来搭配多线程的使用。
在这里插入图片描述

锁的性能图
请添加图片描述

  • 自旋锁

atomic
OSSpinLock
dispatch_semaphore_t (信号量 GCD)

  • 互斥锁

os_unfair_lock(替代OSSpinLock)
pthread_mutex (含recursive实现方案)
@synchronized
NSLock (对象锁)
NSConditionLock (条件锁)
NSCondition
NSRecursiveLock (递归锁)

自旋锁:
自旋锁原理:线程一直是running(加锁——>解锁), 死循环(忙等 do-while)检测锁的标志位,机制不复杂.
优点:自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作.如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁.适用于持有锁较短的程序.
缺点:自旋锁一直占用CPU,在未获得锁的情况下,自旋锁一直运行(忙等状态,询问), 占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低. 自旋锁不能实现递归调用.

互斥锁:
定义:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,
直到上一个执行完成,下一个线程会自动唤醒,然后开始珍惜任务 互斥锁原理:线程会从sleep(加锁)–>running(解锁),
过程中有上下文的切换(主动出让时间片, 线程休眠, 等待下一次唤醒).CPU的抢占,信号的发送等开销.

互斥锁会休眠: 所谓休眠, 即在访问被锁资源时, 调用者线程会休眠,
此时CPU可以调度其他线程工作.直到被锁资源释放锁.此时会唤醒休眠线程.

线程安全举例

假设有一个Person类,其中有一个NSUInteger类型属性age

代码

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, assign) NSUInteger age;

@end

NS_ASSUME_NONNULL_END

当然未赋值的情况下,age默认为0,我们在外部模拟一种多线程访问该实例方法的情况

- (void)withoutLook {
    
    __block Person *p = [Person new];
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < 1000; i++) {
            p.age++ ;
        }
        NSLog(@"%zd\n",p.age);
    }];
    
   
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < 1000; i++) {
            p.age++;
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

有两处代码,在不同的线程调用了p.age++,按照理想状态下应该2000,但分别打印的结果并非如此,(每次执行的结果都不相同,以某一次的值)

打印结果

1205
1199

分析原因

因为不同的线程,所以不确定哪一个会先执行结束,所以分别打印一次,可以看到最大值是1205,并没有达到2000,因为没有加锁,导致不同线程竞争资源。当A线程和B线程都将101赋给了age,但总的有个先来后到,结果就是某一个被覆盖了,按理说两次p.age++应该102,但获取是101,这就出现了误差,多次这样的误差就导致要比2000小很多。

所以这个时候,在多线程访问同一资源时要通过锁来保证同一时刻,仅有一个线程对该资源访问,这样就可以避免上述出现的问题。

iOS 提供了多种锁来解决问题,下面介绍一个各种锁(在下面的例子中我们设置一个统一的次数变量totalCount,设置一个比较大的数值10000)

os_unfair_lock(互斥锁)

自旋锁,苹果在iOS10.0以后用来替代OSSpinLock,需要导入头文件#import <os/lock.h> 具体使用:

苹果NSUUserDefaults保证线程安全使用的是:os_unfair_lock

// 初始化
os_unfair_lock unfair_lock = OS_UNFAIR_LOCK_INIT
// 加锁
os_unfair_lock_lock(&unfair_lock)
 // 解锁
os_unfair_lock_unlock(&unfair_lock) 
 // 尝试加锁
os_unfair_lock_trylock(&unfair_lock)

注意点:
加锁解锁:必须是同一把锁
可能造成死锁:只进行枷锁,没有进行解锁,永远拿不到锁

NSLock

是一种最简单的锁,使用起来也比较简单方便,

代码

- (void)nsLookTest {
    
    __block Person *p = [Person new];
    
    NSLock *lock = [[NSLock alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lock];
            p.age++ ;
            [lock unlock];
        }
        NSLog(@"%zd\n",p.age);
    }];
    
   
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lock];
            p.age++;
            [lock unlock];
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

打印结果

 19357
 20000

NSLock 底层分析

NSLock在Foundation框架,swift的有开源,这里去看一下swift的底层源码!

NSLock是对mutex普通锁的封装,它实现了一个NSLocking的一个协议。

public override init() {
	#if os(Windows)
        	InitializeSRWLock(mutex)
        	InitializeConditionVariable(timeoutCond)
        	InitializeSRWLock(timeoutMutex)
	#else
        	pthread_mutex_init(mutex, nil)
	#if os(macOS) || os(iOS) //iOS
        	pthread_cond_init(timeoutCond, nil)
        	pthread_mutex_init(timeoutMutex, nil)
	#endif
	#endif
    }

可以看到NSLock 其实是pthread_mutex的封装而已

优缺点
1.锁跟锁的嵌套造成死锁如图,
请添加图片描述

要么一直锁下去,要么像2和3互相等待。

总结

NSLock 使用起来也比较简单,用创建的实例对象调用lock/unlock

synchronized:互斥锁

这种锁是所有锁中最为简单的,但性能确是最差的,所以对性能要求不太高的使用情境下,synchronized 不失为一种比较方便的锁。

代码

- (void)synchronizedTest {
    
    __block Person *p = [Person new];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            
            @synchronized (p) {
                p.age++;
            }
            
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
     
            @synchronized (p) {
                p.age++;
            }
       
        }
      NSLog(@"%zd\n",p.age);
    }];
    
}

打印结果

 16881
 20000

可以看出不需要创建锁,一种类似于Swift中调用一个含有尾随闭包的函数,就能实现功能。

synchronized 的内部实现是通过传入的对象,为其分配一个递归锁,储存在哈希表中。

synchronized 注意的地方

  1. synchronized有性能方面的劣势(当发现其他线程执行,当前线程就一直询问,一直在忙等,耗费性能比较高。)
  2. 小括号里面需要传入一个对象类型,基本数据类型不能做参数
  3. 小括号里面这个参数不能为空,如果为nil,就不能保证其锁的功能
  4. 对锁的对象要保证生命周期,如果都用self在enter 查找非常麻烦。

为什么synchronized 的性能这么低?

链表的查询,缓存,下层的不断的查找

代码如下

- (void)synchronizedTest {
    
    __block Person *p = [Person new];
    __block Person *p1 = nil;
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            
            @synchronized (p1) {
                p.age++;
            }
            
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
     
            @synchronized (p1) {
                p.age++;
            }
       
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

打印结果

10075
10072

从打印的结果上来看,两次打印并没有达到两次循环的总和的,也就是传入nil的话,就失去了synchronized的锁的功能

synchronized 底层分析

  1. @synchronized是一个互斥递归锁
  2. 嵌套 - 可重入 - 递归(1.同一个线程重复锁2.多个线程也可以操作)
  3. enter:lockCount++;被锁了所少次(可重入)
  4. objc_sync_enter – lock
  5. objc_sync_exit – unlock

通过xcode转换为汇编代码来看一下@synchronized到底做了什么。
请添加图片描述

源码分析

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

    if (obj) {
       // 开始obj的同步工作,必要时开辟一个关联着obj的互斥递归锁
       // 获取SyncData* data,
        SyncData* data = id2data(obj, ACQUIRE);
        ASSERT(data);
       // 取出data->mutex进行加锁
        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;
}

注释简单翻译如下:

开始obj的同步工作,必要时开辟一个关联着obj的互斥递归锁,当获得锁之后返回OBJC_SYNC_SUCCESS。

通过注释我们就可以得出这样一个结论,@synchronized是一个互斥递归锁

objc_sync_enter主要逻辑如下:

1 obj不为空时,获取SyncData* data,取出data->mutex进行加锁
2. obj为空时,执行obj_sync_nil,通过源码查看其实什么也没有处理。

SyncData

请添加图片描述

id2data

请添加图片描述

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

    if (obj) {
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
           // 进行解锁
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }

    return result;
}

找到对应的SyncData,然后进行解锁,就不做过多的分析了。

优缺点

缺点:

性能差(链表查询,缓存下层的不断查找)

优点

方便简单,安全

其他文章的讲解

pthread

它是一套跨平台的多线程,它是非常强大的多线程锁,可以创建

  • 互斥锁
  • 递归锁
  • 信号量
  • 条件锁
  • 读写锁
  • once锁

在使用时需要导入它的类库
#include <pthread.h>

互斥锁(普通锁)

代码

- (void)ptheadNormalTest {
    
    __block Person *p = [Person new];
    
    __block pthread_mutex_t t;
    pthread_mutex_init(&t,NULL);
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
       
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

可以看到普通互斥锁的创建和使用也是比较简单的,但是需要注意要在合适的地方对其调用进行销毁

 pthread_mutex_destroy(&t);

注意事项
在本例子中都将锁定锁的创建都放在了方法中,但在实际开发中,多线程都是直接调用方法的,所以应使用同一个锁对象。为了保证锁的正常使用,一般将其设置的方法所属对象的一个属性,才能在调用该对象的方法时保证其线程的安全,而不是像例子中那样,我的例子是为了演示效果。

优缺点

1.发现其他线程执行,当前线程休眠(就绪状态)一直等待打开唤醒执行。

递归锁

递归锁的创建方法跟普通锁是同一个方法,不过需要传递一个att参数

代码

- (void)ptheadRecursiveTest {
    
    __block Person *p = [Person new];
    
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr); // 创建锁
    pthread_mutexattr_destroy(&attr);//销毁锁
    __block pthread_mutex_t t = mutex;
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
        }
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            pthread_mutex_lock(&t);
            p.age++;
            pthread_mutex_unlock(&t);
       
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

关于普通锁和递归锁的区别后面在做陈述,这里先做一个简答介绍所的用法

注意事项
1.首先对其属性需要在创建完递归之后释放

 pthread_mutexattr_destroy(&attr);//销毁锁

2.同样也需要注意在改锁对应的对象释放的时候,也要对该锁释放

pthread_mutex_destroy(&t);

使用场景
1.当任务比较复杂时

pthread信号量

pthread的信号量不同于GCD自带的信号量,如前面所说,pthread是跨平台多线程处理API,对信号量处理也提供了相应的使用,其使用方法大概与GCD类似,使用起来也比较简单方便。
代码

- (void)ptheadCondTest {
    
    __block Person *p = [Person new];
    
    __block pthread_mutex_t semaphore = PTHREAD_MUTEX_INITIALIZER;
    __block pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
   
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            pthread_mutex_lock(&semaphore);
            pthread_cond_wait(&cond, &semaphore);
            p.age++;
            pthread_mutex_unlock(&semaphore);
        }
        
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            pthread_mutex_lock(&semaphore);
            p.age++;
            pthread_cond_signal(&cond);
            pthread_mutex_unlock(&semaphore);
       
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

结果

10066

分析
通过上面的方法,可以看到pthread_cond_t是需要搭配pthread普通锁共同使用的,但通过pthread_cond_wait和pthread_cond_signal 来实现信号量的生产和消费。
但与GCD的信号量略有不同。首先pthread_cond_t是需要搭配pthread普通锁一起使用,其次pthread_cond_t是不能设置信号量的个数的。纯粹是一个信号量锁。
pthread_cond_t 也可以成为pthread状态锁,
1.如果是第一个线程先获得调度,在第一个线程内部会调用2.pthread_mutex_lock(&semaphore) 之后,需要等待一个信号量才能继续执行,
此时内部会将其unlock.然后等第二个线程调度,
3.当第二个线程完成后释放一个信号并解锁成功之后,线程1重新获得调度,
4.此时pthread_cond_wait内部重新上锁,然后继续执行线程1的代码,当销毁了这个信号量,下次线程1 在获得调度时,仍然会阻塞,然后周而复始。

执行上面的代码可以看到控制台并没有打印两次p.age 综合刚刚的解释,可以明白控制台打印的是线程2的NSLog,然后线程1消耗了信号量之后并没有其他信号可以使用。所以一直处于阻塞状态,虽然这个没有完全执行p.age到20000,但这种状态时一种自定义的任务调度方式,可以指定的事务交给指定的线程去处理。

可以看出,pthread使用信号量来实现线程安全也是比较方便的,通过一个红来初始化pthread_mutex_t,在涉及锁功能时,pthread_cond_t需要注意与锁的搭配使用。

 pthread_mutex_lock(&semaphore);
 pthread_cond_wait(&cond, &semaphore);
 // CODE
 pthread_mutex_unlock(&semaphore);
 

在调用pthread_cond_wait之前要先上锁,因为没有信号量可以消费的时候pthread_cond_wait回解锁,并在获得新的信号量时再次对其加锁。

pthread_mutex_lock(&semaphore);

// CODE
pthread_cond_signal(&cond);
pthread_mutex_unlock(&semaphore);

这段代码需要在执行完后先释放信号量,再对其加锁,这样线程1才能获取到锁,并且pthread_cond_wait 才能对其重新上锁往下执行,如果先释放锁,可能线程1获取到的锁仍然不能执行,等再释放信号,线程1又的重新获取一遍,更有甚者,此时锁可能又被线程2抢去了。

读写锁

读写锁是一种特殊的自旋锁,将对资源的访问分为读者和写者,读是对资源只进行读访问,而写支队资源写访问。相对于自旋锁来说,这种锁能提高并发性。在多核处理器操作系统中。

  • 允许多个读者访问同一资源,
  • 却只能有一个写者执行写操作,
  • 读写操作不能同时执行

接下来用pthread的读写来接着我们的例子。

代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    pthread_rwlock_init(&_lock, NULL);
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
       for (int i = 0; i < 10; i++) {
           dispatch_async(queue, ^{
               [self readLockTest];
           });
           dispatch_async(queue, ^{
               [self readLockTest];
           });
           dispatch_async(queue, ^{
               [self writeLockTest];
           });
       }
    
}

- (void)readLockTest {
    pthread_rwlock_rdlock(&_lock);
    sleep(2);
    NSLog(@"%s",__func__);
    pthread_rwlock_unlock(&_lock);
}

- (void)writeLockTest {
    pthread_rwlock_wrlock(&_lock);
    sleep(2);
    NSLog(@"%s",__func__);
    pthread_rwlock_unlock(&_lock);
}

-(void)dealloc{
    pthread_rwlock_destroy(&_lock);
}

使用场景
用在例如读写文件中,在上读跟解锁之间执行的操作,写锁跟解锁之间执行写锁操作。

读写锁保障了读写的安全性和有效性,并且更多的是读操作,由于这种处理,导致读写锁性能比普通锁要稍微低一点,但比较安全。

信号量

在iOS 开发中,信号量就是通过GCD来实现的,使用起来也比较简单,信号量属于生产者,消费者模式,这种模式可以用在多个场景中。

代码

- (void)semaphoreTest {
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    __block Person *p = [Person new];
    
   
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            p.age++;
            dispatch_semaphore_signal(semaphore);
        }
        
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            p.age++;
            dispatch_semaphore_signal(semaphore);
       
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

代码解析

每次在访问p.age之前,都会等待一个信号量,才能实现对age的访问,在创建方法中,需要传入一个value 的long 类型的值,表示有多少个信号可以使用。

举例
1.办理银行业务时,银行可以设置多个窗口,这个窗口数就是信号量创建的这个value值,假设5个。相当于有5个资源,
2.没有一个客户办理业务时相当于消耗一个资源,当5个窗口都有业务时,没有可用资源
3.没有可用资源时,又有用户办理业务,需要等某一个窗口释放资源。

当资源充足时当代吗执行到dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);不用等待,而直接消耗一个资源,只有在当前没有可用资源时,才会等待 dispatch_semaphore_signal(semaphore);释放资源。

DISPATCH_TIME_FOREVER 时间参数,在等待一个可用资源是有时间限制的,超过该时间就不去等待资源,而直接执行下面代码。这有一些不合理,因为假设时间很小,在没有获取到信号量资源的时候就去执行代码。可能造成非线性安全的事。但这个是系统安全的也就是并不会造成应用崩溃,
!!! DISPATCH_TIME_FOREVER 目前设置的是一直等下去,这就确保了线程安全。

条件锁( NSConditionLock 与NSCondition)

NSConditionLock

状态锁是一种很常见的锁,在多线程操作中,用户可以指定某线程去执行操作,只需要设置对应的状态即可。

代码

- (void)conditionLockTest {
    
    NSConditionLock *lock = [[NSConditionLock alloc]init];
    __block Person *p = [Person new];
    
    NSInteger thread1 = 1;
    NSInteger thread2 = 0;
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lockWhenCondition:thread1];
            p.age++;
            [lock unlockWithCondition:thread2];
        }
        
        NSLog(@"%zd\n",p.age);
    }];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lockWhenCondition:thread2];
            p.age++;
            [lock unlockWithCondition:thread1];
       
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

打印结结果

 199999
 200000

讲解

这两个线程是轮流使用的,

NSConditionLock的方法

  • lockWhenCondition

在某一中状态下上锁

  • unlockWithCondition

在某一种状态下解锁

就类似于一个公共男女公用厕所,一次只能有一个人使用,男生牌子时男生使用,女生牌子时女生使用。

NSCondition

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

1.[condition lock]:一般用于多线程同时访问,修改同一个数据源,保证在同一个时间内只被访问,修改一次,其他线程的命令需要在lock外等待,只到unlock,才可以访问。
2.[condition unlock]:与 lock同时使用
3.[condition wait]:让当前线程处于等待
4.[condition signal]:CPU告诉线程不用等待,可以继续执行

苹果官网对于NSCondition提供的一段伪代码

lock the condition
while (!(boolean_predicate)) {
    wait on condition
}
do protected work
(optionally, signal or broadcast the condition again or change a predicate value)
unlock the condition

伪代码解析
1.先上锁,当其不满足条件时,使其处于wait状态,紧接着写上一些需要做线程安全的代码,然后释放信号量,或者广播一个状态,足以break刚才的while循环,最后将其解锁,

合理的使用场景说明

1:下载图片使用

当接受到图片消息的时候,需要异步下载,等到图片下载完成之后,同步数据库,方可通知前端更新UI。此时就需要使用NSCondition 的wait

2:视频一帧一帧的渲染屏上时如果中途需要点击暂停和开始的时候可以合理利用它控制线程渲染逻辑

NSCondition 是合理控制监测和控制线程的使用的作用大,合理的使用能够收到一些特需需求的效果

递归锁( NSRecursiveLock)

以上介绍锁的代码中,一个锁只是请求一份资源,而在实际开发中,往往需要嵌套使用

可以看到NSRecursiveLock 其实是pthread_mutex的封装而已

同一个线程中,一个锁还没有解锁,就再次加锁,

NSLock 连续加两次锁,和解锁错误案例

- (void)wrongRecursiveTest {
    
    __block Person *p = [Person new];
    
    NSLock *lock = [[NSLock alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++ ;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"%zd\n",p.age);
    }];
    
   
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

没有输出任何结果,因为我们在使用NSLock 重复上锁时,会造成死锁

NSRecursiveLock 递归锁正确使用案例

- (void)recursiveTest {
    
    __block Person *p = [Person new];
    
    NSRecursiveLock *lock = [[NSRecursiveLock alloc]init];
    
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++ ;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"%zd\n",p.age);
    }];
    
   
    [NSThread detachNewThreadWithBlock:^{
        for (int i = 0; i < self.totalCount; i++) {
            [lock lock];
            [lock lock];
            p.age++;
            [lock unlock];
            [lock unlock];
        }
        NSLog(@"%zd\n",p.age);
    }];
    
}

结果

 182351
 200000

自旋锁(OSSpinLock)

OSSpinLock 目前iOS 有bug并且已经废弃了。

自旋锁(atomic)

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁

可以参考源码objc4的objc-accessors.mm

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    // 用对象的地址加偏移量去取锁
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}
  • get方法实现
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}
  • 给属性加上atomic修饰,可以保证属性的setter和getter都是原子性操作,也就是保证setter和gette内部是线程同步的
重写的setget方法
  • 它并不能保证使用属性的过程是线程安全的
@property (strong, atomic) NSMutableArray *data;

Person *p = [[Person alloc] init];
p.data = [NSMutableArray array];

// 以下操作就不能保证线程安全了
for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [p.data addObject:@"1"];
    });
}

虽然data属性是声明为atomic,但是也只是在p.data(实际上调用了get方法)和p.data = [NSMutableArray array];(实际上调用了set方法)是安全的。但是多条线程同时添加对象时,即[p.data addObject:@“1”];并不能保证线程安全。

  • 锁是有限的,不同的对象,不同的属性有可能取出的是同一把锁
PropertyLocks[slot]; 锁是存在这个里面的
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值