洋仔的博客 洋仔的博客
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

洋仔

奋斗的小青年
首页
  • 个人心法总结

    • 价值心法
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • iOS基础知识
  • 前端
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 投资体系
  • 毛选
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术文档

  • GitHub技巧

  • Nodejs

  • 博客搭建

  • iOS基础知识

    • iOS底层相关

    • Runloop系列

    • Runtime系列

    • 内存管理系列

    • Block系列

    • 线程系列

      • 线程系列
      • 线程系列-锁
        • 多线程中的死锁?
          • 解决死锁的 4 种基本方法
        • iOS中常见的八种锁
          • 为什么需要锁
          • 按照功能来区分锁
          • 常见的锁的类型
          • 总结:
        • 优先级反转
        • iOS 线程间通信
      • 进程系列
      • dispatch_once的实现原理
    • KVC跟KVO系列以及通知中心

    • UI系列

    • 离屏渲染系列

    • 组件化系列跟架构

    • OC跟webview交互系列

    • 持久化系列

    • APP编译系列

    • APP性能优化系列

    • cocoapods系列

    • swift系列

    • Git系列

    • 网络相关

    • 三方库系列

    • 系统原理

    • 总结系列

    • 算法系列

    • 数据结构系列

  • 前端

  • 技术
  • iOS基础知识
  • 线程系列
洋仔
2023-09-26
目录

线程系列-锁

# 多线程中的死锁?

死锁是由于多个线程(进程)在执行过程中,因为争夺资源而造成的互相等待现象,你可以理解为卡主了。产生死锁的必要条件有四个:

互斥条件 : 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

请求和保持条件 : 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

不可剥夺条件 : 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

环路等待条件 :

  1. 存在一种进程资源的循环等待链
  2. 循环等待未必死锁,死锁一定有循环等待

# 解决死锁的 4 种基本方法

1、预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件

2、避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁

3、检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉

4、解除死锁:该方法与检测死锁配合使用

# iOS中常见的八种锁

自旋锁:atomic、OSSpinLock、dispatch_semaphore_t 互斥锁:pthread_mutex、@ synchronized、NSLock、NSConditionLock 、NSCondition、NSRecursiveLock

# 为什么需要锁

在iOS中相信大家都用过多线程,多线程带来的好处显而易见,但是我们需要关注一下多线程有可能带来的问题。假设我们有一个这样的场景,我们有两条线程A和线程B,A线程做的事情是修改这个对象之后读取这个对象的数据,这个时候B线程可能也在修改这个对象。这个时候有两种情况(取决于B线程修改对象的时机):

提示

正常的情况,A线程修改对象以及读取对象之后,B线程才开始修改这个对象。

异常的情况,A线程修改对象之后,B线程立刻修改了这个帝乡,然后A线程读取对象。这个时候A线程读取到的数据就出错了。

这就是我们常说的Data race,当两个线程同时在访问修改同一个块内存的时候,就有可能得到意想不到的结果。

基于上面的前提,我们在出现了用锁来解决问题的方法

# 按照功能来区分锁

# 互斥锁

互斥锁是为了保护一个临界区或者资源不能同时被多个线程访问。当临界区加上互斥锁以后,其他的调用方不能获得锁,只有当互斥锁的持有方释放锁之后其他调用方才能获得锁。

如果调用方在获得锁的时候发现互斥锁已经被其他方持有,那么该调用方只能进入睡眠状态,这样不会占用CPU资源。但是会有时间的消耗,系统的运行时基于CPU时间调度的,每次线程可能有100ms的运行时间,频繁的CPU切换也会消耗一定的时间。

# 自旋锁

自旋锁和互斥锁相似,但是自旋锁不会引起休眠,当自旋锁被别的线程锁定的时候,那么调用方会一直处于等待的状态,用一种生活化的例子来说就像是上厕所,当你要上厕所发现里面已经有人的时候,你就会一直等在外面,直到他出来你就立刻抢占厕所。

由于调用方会一直循环看该自旋锁的的保持者是否已经释放了资源,所以总的效率来说比互斥锁高。但是自旋锁只用于短时间的资源访问,如果不能短时间内获得锁,就会一直占用着CPU,造成效率低下。

# 常见的锁的类型

# OSSpinLock

OSSpinLock是自旋锁,也正是由于它是自旋锁,所以容易发生优先级反转的问题。在ibireme的文章中已经写到,当一个低优先级线程获得锁的时候,如果此时一个高优先级的系统到来,那么会进入忙等状态,不会进入睡眠,此时会一直占用着系统CPU时间,导致低优先级的无法拿到CPU时间片,从而无法完成任务也无法释放锁。除非能保证访问锁的线程全部处于同一优先级,否则系统所有的自旋锁都会出现优先级反转的问题。现在苹果的OSSpinLock已经被替换成os_unfair_lock typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock);

# dispatch_semaphore

dispatch_semaphore主要提供了三个函数:

dispatch_semaphore_create(long value);//创造信号量 
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);//等待信号 
dispatch_semaphore_signal(dispatch_semaphore_t dsema);//发送信号 
1
2
3

dispatch_semaphore是GCD用来同步的一种方式,dispatch_semephore_create方法用户创建一个dispatch_semephore_t类型的信号量,初始的参数必须大于0,该参数用来表示该信号量有多少个信号,简单的说也就是同事允许多少个线程访问。

dispatch_semaphore_wait()方法是等待一个信号量,该方法会判断signal的信号值是否大于0,如果大于0则不会阻塞线程,消耗点一个信号值,执行后续任务。如果信号值等于0那么就和NSCondition一样,阻塞当前线程进入等待状态,如果等待时间未超过timeout并且dispatch_semaphore_signal释放了了一个信号值,那么就会消耗掉一个信号值并且向下执行。如果期间一直不能获得信号量并且超过超时时间,那么就会自动执行后续语句。

# pthread-mutex

pthread-mutex是互斥锁,互斥锁与信号量的机制非常相似,不会处于忙等状态,而是会阻塞线程并休眠。

pthread-mutex提供了几个常用的方法

int pthread_mutex_init(pthread_mutex_t * __restrict, const pthread_mutexattr_t * __restrict);//初始化锁 
int pthread_mutex_lock(pthread_mutex_t *); //加锁 int 
pthread_mutex_unlock(pthread_mutex_t *); //解锁 
1
2
3

pthread_mutex_init方法用来初始化一个锁,需要传入一个pthread_mutex_t的对象,并且需要设置互斥锁的类型。互斥锁有四种类型:

PTHREAD_MUTEX_NORMAL : 默认值普通锁,当一个线程加锁以后,其他线程进入按照优先顺序进入等待队列,并且解锁的时候按照先入先出的方式获得锁。 
PTHREAD_MUTEX_ERRORCHECK : 检错锁,当同一个线程获得同一个锁的时候,则返回EDEADLK,否则与普通锁处理一样。 
PTHREAD_MUTEX_RECURSIVE : 递归锁。这里有别于上面的检错锁,同一个线程可以递归获得锁,但是加锁和解锁必须要一一对应。 
PTHREAD_MUTEX_DEFAULT : 适应锁,等待解锁之后重新竞争,没有等待队列。 
1
2
3
4

# NSLock

NSLock遵循NSLocking协议,同时也是互斥锁,提供了lock和unlock方法来进行加锁和解锁。 NSLock内部是封装了pthread_mutext,类型是PTHREAD_MUTEXT_ERRORCHECK,它会损失一定的性能换来错误提示。

# NSCondition

NSCondition是封装了一个互斥锁和信号量,它把前者的lock以及后者的wait/signal统一到NSCondition对象中,是基于条件变量pthread_cond_t来实现的,和信号量相似,如果当前线程不满足条件,那么就会进入睡眠状态,等待其他线程释放锁或者释放信号之后,就会唤醒线程。类似于生产者和消费者模式

NSCondition *lock = [[NSCondition alloc] init];
    NSMutableArray *array = [[NSMutableArray alloc] init];
    //消费者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [lock lock];
        while (!array.count) {
            [lock wait];
        }
        [array removeAllObjects];
        NSLog(@"array removeAllObjects");
        [lock unlock];
    });
    
    //生产者
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(1);//以保证让线程2的代码后执行
        [lock lock];
        [array addObject:@1];
        NSLog(@"array addObject:@1");
        [lock signal];
        [lock unlock];
    });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# NSRecursiveLock

NSRecursiveLock内部是通过pthread_mutex_lock来实现的,在内部会判断锁的类型,如果是递归锁,就允许递归调用,内部仅仅是将计数器+1。当调用unlock的时候,就将计数器减1。NSRecursiveLock内部使用的pthread_mutex_t的类型是PTHREAD_MUTEXT_RECURSIVE

# NSConditionLock

NSConditonLock 是借助NSCondition,本质上是生产者-消费者模式,NSConditonLock内部持有了一个NSCondition对象和_condition_value属性,当调用- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;初始化的时候会传入一个condition参数,该参数会赋值_condition_value属性。

  • 在NSConditionLockr中,对应的消费者就是- (void)lockWhenCondition:(NSInteger)condition;方法,首先会调用[condition lock],然后开始进入阻塞状态,如果condition=_condition_value,那么就会休眠,直到代码调用- (void)unlockWithCondition:(NSInteger)condition;才会唤起

  • (void)unlockWithCondition:(NSInteger)condition;就是对应的生产者方法,内部会设置condition=_contion_value,并且发送广播告诉所有的消费者,表示生产完成,然后调用[condition unlock]释放锁

# @synchronized

@synchronized是OC层面上的锁,是所有的锁之中性能最差的。 @synchronized后面紧跟一个OC对象,实际上是将这个对象当做锁来使用。这是通过一个哈希表来实现的,OC在底层维护了一个互斥锁的数组,通过对象的哈希值去得到对象的互斥锁。

# 总结:

经过上面的分析我们知道锁的性能由高到低分别是 OSSpinLock(已经不推荐使用)->dispatch_semaphore->pthread_mutext->NSLock->NSCondition->NSRecursiveLock->NSConditonLock->@synchronized 我们再来梳理一下它们的关系:

dipatch_semaphore是GCD同步的一种方式,通过dispatch_semaphore_t信号量来实现。 2.pthread_mutex是互斥锁,提供了四种不同类型,不会像自旋锁一样忙等,而是会进入休眠等待。 3.NSLock是封装了prthread_mutex,锁的类型是PTHREAD_MUTEX_ERRORCHECK,也就是当同一个线程获得同一个锁的时候,会返回错误。 4.NSCondition是基于条件变量pthread_cond_t实现的,和信号量相似,当不满足条件的时候就会进入休眠等待,知道condition对象发出signal信号,才会被唤醒执行。 5.NSRecursiveLock是递归锁,同样是封装了pthread_mutex来实现,但是锁的类型是PTHREAD_MUTEX_RECURSIVE,允许统一递归获得锁,但是要注意加锁和解锁要一一对应。 6.NSConditionLock是基于NSCondition实现的,同样也是生产者和消费者模式。 7.@synchronized是OC层面的锁,传入一个OC对象,通过对象的哈希值来作为标识符得到互斥锁,存入到一个数组里面。

# 优先级反转

优先级反转(Poiority Inversion) 指高优先级任务需要等待低优先级任务执行完成才能继续执行,这种情况下优先级被反转了。

举例:有三个线程分别为:A、B、C。优先级A > B > C,线程A和B处于挂起状态,等待某一事件发生,线程C正在运行,此时任务C开始使用共享资源Source。在使用Source时,线程A等待事件到来,线程A转为就绪态,因为线程A优先级比线程C高,所以线程A会立即执行。当线程A要使用共享资源Source时,由于共享资源Source正在被线程C使用,因此线程A被挂起,线程C开始运行。如果此时中等优先级线程B等待事件到来,则线程B转为就绪态。由于线程B优先级比线程C高,因此线程B开始运行,直到其运行完毕,线程C才开始运行。直到线程C释放共享资源Source后,线程A才得以执行。在这种情况下,优先级发生了翻转,线程B先于线程A运行。

# 优先级反转会造成什么后果

  • 低优先级的任务比高优先级的任务先执行,导致任务的错乱,逻辑错乱;
  • 可能造成系统崩溃;
  • 死锁;优先级低的线程迟迟得不到调度,具有高优先级的线程不能执行,死锁;

# 怎么避免线程优先级反转

如果当前线程因等待某线程上正在进行的 操作如(block1)而受阻,而系统知道block1的所在的目标线程,系统会通过提高相关线程的优先级来解决优先级反转的问题 (如线程A在尝试获取共享资源而被挂起的期间内,将线程C的优先级提升到同线程A的优先级,等线程C处理结束,降回原优先级,这样能防止C被B抢占)。

如果不知道block1所在的目标线程,则无法知道应该提高谁的优先级,也就无法解决反转的问题,如信号量。

# 使用信号量可能会造成线程优先级反转,且无法避免

QoS (Quality of Service),用来指示某任务或者队列的运行优先级;

1、记录了持有者的api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如 dispatch_sync, 如果系统不知道持有者所在的线程,则无法知道应该提高谁的优先级,也就无法解决反转问题。 2、慎用dispatch_semaphore 做线程同步 dispatch_semaphore 容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);

3、dispatch_semaphore 不能避免优先级反转的原因 在调用dispatch_semaphore_wait() 的时候,系统不知道哪个线程会调用 dispatch_semaphore_signal()方法,系统无法知道owner信息,无法调整优先级。dispatch_group 和semaphore类似,在调用enter()方法的时候,无法预知谁会leave(),所以系统也不知道owner信息

# iOS 线程间通信

  1. NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法
-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOl)wait;

-(void)performSelector:(SEL)aSelector onThread:(NSThread*)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

1
2
3
4
  1. GCD一个线程传递数据给另一个线程,如:
dispatch_async(dispatch_get_main_queue(), ^{
      
      NSLog(@"setting---%@ %@", [NSThread currentThread], image);
      
      [self.button setImage:image forState:UIControlStateNormal];
  });
1
2
3
4
5
6
编辑 (opens new window)
上次更新: 2023/10/28, 15:22:03
线程系列
进程系列

← 线程系列 进程系列→

最近更新
01
数组
10-25
02
数组双指针系列之对撞指针
10-25
03
数组双指针系列之快慢指针
10-25
更多文章>
Theme by Vdoing | Copyright © 2019-2024 Evan Xu | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式