IOS中的锁

本文详细解释了线程安全的概念,展示了nonatomic与atomic的区别,通过代码实例说明atomic并不自动保证线程安全,并介绍了iOS中各种锁的类型如OSSpinLock、NSLock、NSRecursiveLock和dispatch_semaphore,强调了在实际开发中正确使用锁的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在学习ios中的锁之前,我们先理了解线程安全的概念。

线程安全(thread safety)
  • 线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
  • 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

我们之前声明属性时学习过使用atomic与nonatomic,一个原子性与一个非原子性,我们一般都是使用非原子性,但却不知道这两个有什么区别与影响,其实这两个也是涉及到了ios中的锁。

简单来说:

  • nonatomic 不会对生成的 getter、setter 方法加同步锁(非原子性)
  • atomic 会对生成的 getter 、setter 加同步锁(原子性)
但是使用 atomic 一定是线程安全的么?

验证代码:

- (void)testAtomic{//atomic是否线程安全验证
        //两线程异步执行
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                self.name = @"张三";
                NSLog(@"张三是%@",self.name);
            }
        });
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (1) {
                self.name = @"李四";
                NSLog(@"李四是%@",self.name);
            }
        });
}

验证结果:

021-02-24 17:31:48.673200+0800 demo[16665:3807736] 李四是李四
2021-02-24 17:31:48.673400+0800 demo[16665:3807734] 张三是张三
2021-02-24 17:31:48.673537+0800 demo[16665:3807736] 李四是李四
2021-02-24 17:31:48.673793+0800 demo[16665:3807734] 张三是张三
2021-02-24 17:31:48.674028+0800 demo[16665:3807736] 李四是李四
2021-02-24 17:31:48.674219+0800 demo[16665:3807734] 张三是张三
2021-02-24 17:31:48.674430+0800 demo[16665:3807736] 李四是李四
2021-02-24 17:31:48.724642+0800 demo[16665:3807734] 张三是张三
2021-02-24 17:31:48.724642+0800 demo[16665:3807736] 李四是张三
2021-02-24 17:31:48.724791+0800 demo[16665:3807734] 张三是张三

所以答案是不是线程安全的。

nonatomic的内存管理语义是非原子性的,非原子性的操作本来就是线程不安全的,而atomic的操作是原子性的,但是并不意味着它是线程安全的,它会增加正确的几率,能够更好的避免线程的错误,但是它仍然是线程不安全的。

当使用nonatomic的时候,属性的setter,getter操作是非原子性的,所以当多个线程同时对某一属性读和写操作时,属性的最终结果是不能预测的。

当使用atomic时,虽然对属性的读和写是原子性的,但是仍然可能出现线程错误:当线程A进行写操作,这时其他线程的读或者写操作会因为该操作而等待。当A线程的写操作结束后,B线程进行写操作,然后当A线程需要读操作时,却获得了在B线程中的值,这就破坏了线程安全,如果有线程C在A线程读操作前release了该属性,那么还会导致程序崩溃。所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全。

也就是要注意:atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。如下列所示:

比如:@property(atomic,strong)NSMutableArray *arr;

如果一个线程循环的读数据,一个线程循环写数据,那么肯定会产生内存问题,因为这和setter、getter没有关系。如使用[self.arr objectAtIndex:index]就不是线程安全的。好的解决方案就是加锁。

ios中的锁有许多,但是在不同情况下用不同的锁却是有不同的效果,首先上一张锁的性能排行图。
image.png
可以看到OSSpinLock是性能最高的,@ synchronized是性能最低的。

iOS开发中常用的锁

  • OSSpinLock 自旋锁
  • pthread_mutex 互斥锁(C语言)
  • @synchronized
  • NSLock 对象锁
  • NSRecursiveLock 递归锁
  • NSConditionLock 条件锁
  • dispatch_semaphore 信号量实现加锁(GCD)
    ###OSSpinLock自旋锁
    OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
    使用:
//导入头文件
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁(如果不需要等待,就直接加锁,返回true。如果需要等待,就不加锁,返回false)
BOOL res = OSSpinLockTry(lock);
//加锁
OSSpinLockLock(lock);
//解锁
OSSpinLockUnlock(lock);

OSSpinLock 自旋锁,性能最高的锁。它的缺点是当等待时会消耗大量 CPU 资源,不太适用于较长时间的任务。 博客 不再安全的 OSSpinLock 中说明了OSSpinLock已经不再安全,暂不建议使用。

新版 iOS 中,系统维护了 5 个不同的线程优先级 /QoS: background , utility , default , user-initiated , user-interactive 。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock 。

具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU 。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock 。这并不只是理论上的问题, libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock 。
###NSLock锁

  • NSLock是对mutex普通锁的封装
    #####API
@protocol NSLocking

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

@end

NSLock准守这个协议,锁可以直接使用,另外,还有tryLock和lockBeforeDate

- (void)lock; //加锁
- (void)unlock; //解锁
- (BOOL)tryLock; //尝试加锁,如果加锁失败,就返回NO,加锁成功就返回YES
- (BOOL)lockBeforeDate:(NSDate *)limit; //在给定的时间内尝试加锁,加锁成功就返回YES,如果过了时间还没加上锁,就返回NO。

NSRecursiveLock 递归锁

  • NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
  • 有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。
API
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

NSRecursiveLock准守这个协议,可以直接使用,另外,还有tryLock和lockBeforeDate
#####API

- (void)lock; //加锁
- (void)unlock; //解锁
- (BOOL)tryLock; //尝试加锁,如果加锁失败,就返回NO,加锁成功就返回YES
- (BOOL)lockBeforeDate:(NSDate *)limit; //在给定的时间内尝试加锁,加锁成功就返回YES,如果过了时间还没加上锁,就返回NO。

NSConditionLock 条件锁

  • NSConditionLock是对NSCondition的进一步封装,可以设置条件具体值
API
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;//当条件condition时,加锁
- (BOOL)tryLock;//尝试加锁,如果加锁失败,就返回NO,加锁成功就返回YES
- (BOOL)tryLockWhenCondition:(NSInteger)condition;//当条件condition时尝试加锁,如果加锁失败,就返回NO,加锁成功就返回YES
- (void)unlockWithCondition:(NSInteger)condition;//当条件condition时,解锁
- (BOOL)lockBeforeDate:(NSDate *)limit; //在给定的时间内尝试加锁,加锁成功就返回YES,如果过了时间还没加上锁,就返回NO。
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; //当条件condition时,在给定的时间内尝试加锁,加锁成功就返回YES,如果过了时间还没加上锁,就返回NO。

@synchronized

  • @synchronized 其实是一个 OC 层面的锁, 主要是通过牺牲性能换来语法上的简洁与可读性。
  • @synchronized 是我们平常使用最多的但是性能最差的。
    使用:
@synchronized(self) {
    //需要执行的代码块
}

@synchronized(self) 指令使用的 self 为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果 self 改成其它标识符,就不会阻塞线程。

dispatch_semaphore_t

dispatch_semaphore_t GCD中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量 +1;每当发送一个等待信号时信号量 -1,;如果信号量为 0 则信号会处于等待状态,直到信号量大于 0 开始执行。

API
/*! 
 * @param value
 *信号量的起始值,当传入的值小于零时返回NULL
 * @result
 * 成功返回一个新的信号量,失败返回NULL
 */
dispatch_semaphore_t dispatch_semaphore_create(long value)

/*!
 * @discussion
 * 信号量减1,如果结果小于0,那么等待队列中信号增量到来直到timeout
 * @param dsema
 * 信号量
 * @param timeout
 * 等待时间
 * 类型为dispatch_time_t,这里有两个宏DISPATCH_TIME_NOW、DISPATCH_TIME_FOREVER
 * @result
 * 若等待成功返回0,timeout返回非0
 */
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

/*!
 * @discussion
 * 信号量加1,如果之前的信号量小于0,将唤醒一条等待线程
 * @param dsema 
 * 信号量
 * @result
 * 唤醒一条线程返回非0,否则返回0
 */
long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

pthread_mutex互斥锁

  • C语言定义下多线程加锁方式。
  • mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
    使用:
//导入头文件
#import <pthread.h>
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);

其中锁的类型有四种

#define PTHREAD_MUTEX_NORMAL		0   //一般的锁
#define PTHREAD_MUTEX_ERRORCHECK	1	// 错误检查
#define PTHREAD_MUTEX_RECURSIVE		2  //递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL  //默认
  • pthread_mutex 还可以创建条件锁,提供了和 NSCondition 一样的条件控制,初始化互斥锁同时使用 pthread_cond_init 来初始化条件数据结构
    // 初始化
    int pthread_cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
    // 等待(会阻塞)
    int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mut);
    // 定时等待
    int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mut, const struct timespec *abstime);
    // 唤醒
    int pthread_cond_signal (pthread_cond_t *cond);
    // 广播唤醒
    int pthread_cond_broadcast (pthread_cond_t *cond);
    // 销毁
    int pthread_cond_destroy (pthread_cond_t *cond);
参考博客

https://www.douban.com/note/486901956/
简书
https://juejin.cn/post/6844904132990631944#heading-8
https://juejin.cn/post/6844903520257343495#heading-11
https://juejin.cn/post/6844903914190405640#heading-43

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值