iOS底层笔记--多线程

558 阅读7分钟

本文属笔记性质,是对kirito_song冰凌天两位文章学习和转载

文章具体地址:
MJiOS底层笔记--多线程
小码哥iOS学习笔记第二十天: 多线程的安全隐患

常见的多线程方案

GCD (详细使用可看文章:iOS 多线程:『GCD』详尽总结

同步/异步和串行/并发 四种组合

  • dispatch_sync(serial_queue,^{//任务}) //同步串行
  • dispatch_async(serial_queue,^{//任务}) //异步串行
  • dispatch_sync(concurrent_queue,^{//任务}) //同步并行
  • dispatch_async(concurrent_queue,^{//任务}) //异步并行

1.同步串行

- (void)viewDidLoad {
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self doSomething];
    });
}

此写法造成死锁,死锁的原因:队列引起的循环等待

  如果在主线程中运用主队列同步,也就是把Block放到了主线程的队列中。
  而同步对于Block是立刻执行的,所以当把Block放进主队列时,它就会立马执行。
  但是此时主线程正在处理 viewDidLoad 方法,Block需要等 viewDidLoad 执行完才能执行。
  viewDidLoad 执行到Block的时候,又要等Block执行完才能往下执行。
  这样 viewDidLoad 方法和第一个任务就开始了互相等待,形成了死锁。

- (void)viewDidLoad {
    dispatch_sync(serialQueue, ^{
        [self doSomething];
    });
}

此写法没有问题

dispatch_barrier_async

dispatch_barrier_async方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行

栅栏函数的官方标注里边有提到。
* @discussion
* Submits a block to a dispatch queue like dispatch_async(), but marks that
* block as a barrier (relevant only on DISPATCH_QUEUE_CONCURRENT queues).
仅适用于DISPATCH_QUEUE_CONCURRENT 创建的并发队列。

怎样利用GCD实现多读单写

  • 补充:dispatch_set_target_queue 的两种用途 (dispatch_set_target_queue 的两种用途
    • 用法一:改变dispatch_queue_create函数生成的Dispatch Queue的优先级

        dispatch_queue_t serialQueue = dispatch_queue_create("com.ios.oc",NULL);
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND,0);
        dispatch_set_target_queue(serialQueue, globalQueue);
        
        第一个参数是要执行变更的队列(不能指定主队列和全局队列)
        第二个参数是目标队列(指定全局队列)
      
        dispatch_queue_create函数生成的DisPatch Queue不管是Serial DisPatch
        Queue还是Concurrent Dispatch Queue,执行的优先级都与默认优先级的Global
        Dispatch queue相同,如果需要变更生成的Dispatch
        Queue的执行优先级则需要使用dispatch_set_target_queue函数。
      
    • 用法二:目标队列可以成为原队列的执行阶层

       如果在多个Serial Dispatch Queue中用dispatch_set_target_queue函数指定目标位
       某一个Serial Dispatch Queue,那么原先本应并行执行的多个Serial Dispatch Queue,
       在目标Serial Dispatch Queue上只能同时执行一个处理。
       
       示例:  
       dispatch_queue_t serialQueue1 = dispatch_queue_create("com.ios.chen", NULL); 
       dispatch_queue_t serialQueue2 = dispatch_queue_create("com.ios.zhen", NULL);
       dispatch_queue_t targetQueue = dispatch_queue_create("com.ios.target", NULL);
       dispatch_set_target_queue(serialQueue1, targetQueue);
       dispatch_set_target_queue(serialQueue2, targetQueue);
       dispatch_async(serialQueue1, ^{
          NSLog(@"s1 in");
          [NSThread sleepForTimeInterval:1.f];
          NSLog(@"s1 out");
       });
       dispatch_async(serialQueue2, ^{
          NSLog(@"s2 in");
          [NSThread sleepForTimeInterval:1.f];
          NSLog(@"s2 out");
       });
       执行顺序:s1 in   s1 out   s2 in   s2 out
      
      

NSOperation(详细使用可看文章:www.jianshu.com/p/4b1d77054…)

需要和NSOperationQueue配合使用来实现多线程

  • 可添加完成的代码块,在操作完成后执行。
  • 添加操作之间的依赖关系,方便的控制执行顺序。
  • 设定操作执行的优先级。
  • 可以很方便的取消一个操作的执行。
  • 使用 KVO 观察对操作执行状态的更改
    • isReady : 任务是否处于就绪
    • isExecuting : 任务是否执行中
    • isFinished : 任务是否已经完成
    • isCancelled : 任务是否已经取消
  • 设置最大并发量

状态控制

如果只重写main方法,底层控制变更任务执行完成状态,以及退出任务
如果重写了start方法,自行控制任务状态

NSThread (详细使用可看文章:www.jianshu.com/p/cbaeea536…

iOS中的线程同步方案

1.自旋锁

等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源

OSSpinLock

#import <libkern/OSAtomic.h>

@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock lock;
@end

@implementation OSSpinLockDemo
- (instancetype)init
{
    if (self = [super init]) {
        self.lock = OS_SPINLOCK_INIT;
    }
    return self;
}


- (void)test
{
    OSSpinLockLock(&_lock);
    
    //同步操作
    
    OSSpinLockUnlock(&_lock);
}
@end

优先级反转

OSSpinLock目前已经不再安全,可能会出现优先级反转问题

  • 一个程序中可能会有多个线程, 但是只有一个CPU

  • CPU给线程分配资源, 让他们穿插的执行,比如有三个线程thread1、thread2和thread3

  • CPU通过分配, 让thread1执行一段时间后, 接着让thread2执行一段时间,然后再让thread3执行一段时间,这样就给了我们有多个线程同时执行任务的错觉

  • 而线程是有优先级的

    • 如果优先级高, CPU会多分配资源, 就会有更多的时间执行
    • 如果优先级低, CPU会减少分配资源, 那么执行的就会慢
  • 那么就可能出现低优先级的线程先加锁,但是CPU更多的执行高优先级线程, 此时就会出现类似死锁的问题

    • 假设通过OSSpinLock给两个线程thread1thread2加锁 thread1优先级高, thread2优先级低
    • 如果thread2先加锁, 但是还没有解锁, 此时CPU切换到thread1
    • 因为thread1的优先级高, 所以CPU会更多的给thread1分配资源,这样每次thread1中遇到OSSpinLock都处于使用状态
    • 此时thread1就会不停的检测OSSpinLock是否解锁, 就会长时间的占用CPU 这样就会出现类似于死锁的问题

2.互斥锁

os_unfair_lock

  • os_unfair_lock用于取代不安全的OSSpinLock, 从iOS10开始才支持
  • 从底层调用看, 等待os_unfair_lock锁的线程会处于休眠状态, 并非忙等
  • 需要导入头文件#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁, 如果lock已经被使用, 加锁失败返回false, 如果加锁成功, 返回true
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);

pthread_mutex -- PTHREAD_MUTEX_DEFAULT&&PTHREAD_MUTEX_NORMAL

  • mutex叫做互斥锁,等待锁的线程会处于休眠状态
  • PTHREAD_MUTEX_DEFAULTPTHREAD_MUTEX_NORMAL下的 pthread_mutex 会产生互斥效果
  • 需要导入头文件#import <pthread.h>
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t pthread;
pthread_mutex_init(&pthread, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 销毁锁
pthread_mutex_destroy(&pthread);

  • 用法

NSLock

  • NSLock是对mutexnormal锁的封装

  • 用法

3.递归锁

  • 允许同一个线程对一把锁进行重复加锁

pthread_mutex -- PTHREAD_MUTEX_RECURSIVE

  • 初始化
  • 用法

NSRecursiveLock

  • NSRecursiveLock也是对mutex递归锁的封装,APINSLock基本一致

@synchronized

  • @synchronized是对mutex递归锁的封装
  • @synchronized(obj)内部会生成obj对应的递归锁并存在hash表中,然后进行加锁、解锁操作。性能最差

4.条件锁

  • 可以让一个线程在加锁途中等待另一个线程完成某个动作后继续加锁执行,类似消费者在商场等着商家调货,然后继续购买。
  • 在上锁状态下可以暂时将锁放开,休眠并等待某个条件
  • 当其他线程对条件发送信号,唤醒继续加锁并执行

pthread_mutex - pthread_cond_t

  • 用法

  • 当点击屏幕时, 会在array中移除最后一个元素和添加一个新元素, 代码中可以看到, 使用不同线程调用__remove__add两个方法
  • 现在的需求是, 只有在array不为空的情况下, 才能执行删除操作, 如果直接运行, 那么可能会先调用__remove在调用__add, 那么就与需求相违背
  • 所以, 我们可以使用条件对两个方法进行优化
  • 运行程序, 点击屏幕, 可以看到程序先进入__remove方法, 但是却在__add中添加新元素之后再移除元素

NSCondition

  • NSCondition是对mutexcond的封装
wait:进入等待状态
waitUntilDate::让一个线程等待一定的时间
signal:唤醒一个等待的线程
broadcast:唤醒所有等待的线程
  • 用法

NSConditionLock

  • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
@interface NSConditionLock : NSObject <NSLocking> {
@private
    void *_priv;
}

// 初始化, 同时设置 condition
- (instancetype)initWithCondition:(NSInteger)condition;

// condition值
@property (readonly) NSInteger condition;

// 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (void)lockWhenCondition:(NSInteger)condition;
// 尝试加锁
- (BOOL)tryLock;
// 尝试加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
// 解锁, 同时设置NSConditionLock实例中的condition值
- (void)unlockWithCondition:(NSInteger)condition;
// 加锁, 如果锁已经使用, 那么一直等到limit为止, 如果过时, 不会加锁
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 加锁, 只有NSConditionLock实例中的condition值与传入的condition值相等时, 才能加锁, 时间限制到limit, 超时加锁失败
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
// 锁的name
@property (nullable, copy) NSString *name;

@end
  • 用法

5.dispatch_semaphore_t

  • 可以使用dispatch_semaphore_t设置信号量为1, 来控制同意之间只有一条线程能执行, 实际代码如下
    运行程序, 点击屏幕, 可以看到打印结果正确