iOS知识梳理 - 多线程(2)API梳理

更新于 2019-10-22  约 19 分钟

在iOS平台下使用多线程,一般来讲有四套方案:

  1. 基于c语言的Pthreads接口,这是POSIX的线程标准,在Linux/unix/windows平台上都有实现,c语言编程时使用广泛,但在iOS开发中使用较少。暴露的接口比较底层,功能完善,相应的,就需要程序员管理线程的生命周期,使用相对比较麻烦。
  2. NSThread,objc的线程接口,基本可以理解是Pthreads的面向对象封装。面向对象后管理起来容易一点,但仍然需要付出一定的手动管理代价。
  3. GCD(Grand Central Dispatch),是一种基于线程池的多任务管理接口。GCD抽象出了队列和任务的概念,开发者只需要把任务丢到合适的队列里,不用关注具体的线程管理,GCD会自动管理线程的生命周期。剥离了线程使用的很多细节,接口方便友好,实际使用比较广泛。
  4. NSOperation/NSOperationQueue,大体上相当于GCD的Objc封装,提供了一些在GCD中不容易实现的特性,如:限制最大并发数量,操作之间的依赖关系等。由于使用比GCD更麻烦,因此使用并不特别广泛。但涉及限制并发数和操作依赖的时候肯定是用NSOperation更好的。

Pthreads

Pthreads是POSIX标准的线程,这套标准规定了一个标准意义上的线程应当具备的完整能力。这里列举其主要接口:

  1. 创建

    • int pthread_create (pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg)
  2. 结束线程

    • void pthread_exit (void *retval):线程内退出
    • int pthread_cancel (pthread_t thread):从外部终止一个线程
  3. 阻塞

    • int pthread_join(pthread_t thread, void **retval)
    • 阻塞等待另一个线程结束(exit)
    • unsigned sleep(unsigned seconds)
    • 睡一会儿
  4. 分离

    • int pthread_detach(pthread_t tid)
    • 默认地,一个pthread执行完后不会释放资源,而是保留其执行结束的状态,detach状态下则会执行结束立即释放。也可以在创建的时候带个参数,创建detach的线程。
    1. 锁放后面一起对比。
    2. 互斥锁pthread_mutex_
    3. 自旋锁pthread_spin_
    4. 读写锁pthread_rwlock_
    5. 条件锁pthread_con_

锁放后面一起对比。

参考iOS多线程Pthreads篇

NSThread

线程的Objc封装,用得不多。现在应该都是基于pthread封装的。

// 1. 创建线程
- (instancetype)init;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;
- (instancetype)initWithBlock:(void (^)(void))block ;

// 创建detach线程,即执行完就会立即释放
+ (void)detachNewThreadWithBlock:(void (^)(void))block;
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

// 2. 结束线程
+ (void)exit;//结束当前线程
- (void)start;//结束那个线程

// 3. 阻塞
// NSThread没有类似join的方法,可以通过锁来实现。
// 不过它会sleep
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

关于优先级

一开始线程的优先级是通过threadPriority来控制的,是个0~1.0之间double类型的属性。不过iOS8之后,苹果推动使用NSQualityOfService来代表优先级,主要原因还是希望开发者忽略底层线程相关的细节,可以看到Qos的描述已经是偏应用上层的划分方式了:

typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
}

GCD

Grand Central Dispatch (GCD)是 Apple 开发的一个多核编程的解决方法,基本上可以理解为iOS平台下的线程池接口。

GCD中有4个关键概念:

  • 同步调用:dispatch_sync
  • 异步调用:dispatch_async
  • 串行队列:DISPATCH_QUEUE_SERIAL
  • 并发队列:DISPATCH_QUEUE_CONCURRENT

分派一个任务的时候,有同步和异步两种方式,关注的是当前上下文跟调用的任务之间的关系,同步调用则阻塞等待调用完成后才继续往下执行,就像同步的网络请求一样;异步调用则直接往下执行,dispatch出去的task自己玩去吧。

GCD中,任务是被放到队列里然后按一定的规则调度到不同的线程里执行的。这里的队列分为串行队列和并发队列两种,如果是串行队列,那么丢进去的任务是串行执行的,如果是并发队列,那么丢进去的任务是并发执行的。

基本使用

GCD为我们提供了几个默认队列:

  1. 主队列
dispatch_queue_t queue = dispatch_get_main_queue();

主队列是个串行队列,和主线程是绑定的。

  1. 全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

系统提供了四个不同优先级的全局并发队列,通常我们使用default级别。

我们也可以自己创建队列:

// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue1", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue2", DISPATCH_QUEUE_CONCURRENT);

使用时,需要在主线程执行的任务丢到main_queue,主要是UI或其它只能在主线程调用的系统方法,这没什么问题;

一些有序但不需要放到主线程的,我们可以自己创建串行队列进行处理。

并发任务,通常dispatch_async到default级别的global_queue里即可。一般不需要自己创建并发队列。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
    // do something
});

我们较少使用dispatch_sync这个接口,除非上下文对顺序有强依赖并且不得不在另一个队列执行的逻辑。

例如imageNamed:在iOS9以前不是线程安全的,我们要抛到主线程执行,但下文对其有较强的依赖:

// ...currently in a subthread
__block UIImage *image;
dispatch_sync_on_main_queue(^{
    image = [UIImage imageNamed:@"Resource/img"];
});
attachment.image = image;

但更多的时候我们为了避免使用dispatch_sync,会使用dispatch_async到目标队列,执行完再dispatch_async回来。

dispatch_sync的使用一定要慎之又慎。如果在一个串行队列dispatch到当前队列,就会造成死锁。因为当前任务阻塞住了,等待这个block执行完才继续执行,但gcd的串行机制,这个block还在排队,必须等到当前任务执行完才会开始执行,因此死锁。如果是并发队列那么不会造成死锁。但如果block里面的逻辑又涉及其它的锁机制,这里的情况可能就会非常复杂。因此dispatch_sync这个东西尽量少用,不得不用时一定要梳理得非常清楚。

GCD的能力很丰富,除了上面的基本用法外,还有很多场景会用到:

定时器

由于NSTimer容易造成循环引用,并且不改RunLoopMode时竟然会被界面滑动给挤掉,子线程不开启Runloop不能使用,种种限制比较多,有时候我们会用GCD提供的相关能力替代。

一次性定时器

可以用dispatch_after,如下:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)),dispatch_get_main_queue(), ^{
});

gcd提供了时间类型dispatch_time_t,看起来是个Int64好像跟后面的(int64_t)(3 * NSEC_PER_SEC)是一回事一样,在模拟器上用dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)减去DISPATCH_TIME_NOW,结果正好接近1 * NSEC_PER_SEC,看着跟真的一样...当时还正好在网上看到有个demo对dispatch_time_t加加减减来算时间...我就信了...实在是太年轻了

看一下官方的解释

/*!
 * @typedef dispatch_time_t
 *
 * @abstract
 * A somewhat abstract representation of time; where zero means "now" and
 * DISPATCH_TIME_FOREVER means "infinity" and every value in between is an
 * opaque encoding.
 */
typedef uint64_t dispatch_time_t;

dispatch_time_t是个时间的抽象表示,从现在到永远在uint64上的映射,这个映射还是不透明的,也就是说大概率不是个均匀映射,反正,别指望着随便用了。

上面提到的1s的例子,只是个巧合,在模拟器上对得上,但是在真机上就不行了。而我当初看到的那个加加减减的demo,其实是swift写的,在swift下对应的类型重载了运算符所以可以直接用+/-运算。

总之,dispatch_time_t只能结合gcd的接口使用,它的值对应着一个时间但跟我们理解的时分秒这种时间单位完全没有关系。我们算时间还是老老实实用NSTimerInterval。

循环的定时器

GCD提供了一种称为Source的机制,将source和一个任务关联,可以通过触发这个source的方式执行关联的任务。timer就是其中的一种source。其它的source实际使用非常少。

使用方式如下

_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 1*NSEC_PER_SEC), 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
    // do something
});
dispatch_resume(_timer);

Dispatch Group

dispatch_group可以把多个任务合成一组,于是可以知道一组任务何时全部执行完。

NSLog(@"--- 开始设置任务 ----");
// 因为 dispatch_group_wait 会阻塞线程,所以创建一个新的线程,用来完成任务
// 同时用异步的方式向新线程(tasksQueue)中添加任务
dispatch_queue_t tasksQueue = dispatch_queue_create("tasksQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(tasksQueue, ^{
    // 真正用来完成任务的线程
    dispatch_queue_t performTasksQueue = dispatch_queue_create("performTasksQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    for (int i = 0; i < 3; i++) {
        // 入组之后的 block 会被 group 监听
        // 注意:dispatch_group_enter 一定和 dispatch_group_leave 要配对出现
        dispatch_group_enter(group);
        dispatch_async(performTasksQueue, ^{
            NSLog(@"开始第 %zd 项任务", i);
            [NSThread sleepForTimeInterval:(3 - i)];
            dispatch_group_leave(group);
            NSLog(@"完成第 %zd 项任务", i);
        });
    }
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"全部任务完成");
    });
});
NSLog(@"--- 结束设置任务 ----");

dispatch_once

结合dispatch_once_t,保证只执行一次,由于语义比较清晰,现在是实习单例的最佳写法。

+ (SomeManager *)sharedInstance
{
    static SomeManager *manager = nil;
    static dispatch_once_t token;

    dispatch_once(&token, ^{
        manager = [[SomeManager alloc] init];
    });
    return manager;
}

dispatch_barrier_async

栅栏函数,只有配合并发队列才有意义。当前面的任务都执行完后,当前任务才会开始执行,当前任务执行完后,后面的任务才会开始执行。

- (void)barrier
{
  //同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用
    dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}

NSOperationQueue

NSOperationQueue/NSOperation本身是早于GCD推出的,不过后来基于GCD重写了,可以理解为基于GCD的面向对象封装,类似的,也沿用了任务/队列的概念。

NSOperationQueue/NSOperation对比GCD的优点:

  1. 可添加完成的代码块,在操作完成后执行。
  2. 添加操作之间的依赖关系,方便的控制执行顺序。
  3. 设定操作执行的优先级。
  4. 可以很方便的取消一个操作的执行。
  5. 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。

基本使用如下:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
  for (int i = 0; i < 2; i++) {
    [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
    NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
  }
}];
[queue addOperation:operation];

可以看出,比GCD用起来麻烦了很多,所以通常只有涉及到上面的几点优势场景才会用。

逐个来看一下。

  1. 可添加完成的代码块,在操作完成后执行。

    • NSOperation的方法
    • - (void)setCompletionBlock:(void (^)(void))block; completionBlock 会在当前操作执行完毕时执行 completionBlock。`
  2. 添加操作之间的依赖关系,方便的控制执行顺序。

    • 仍是NSOperation的方法
    • - (void)addDependency:(NSOperation *)op;
    • - (void)removeDependency:(NSOperation *)op;
  3. 设定操作执行的优先级。

    • NSOpetation的属性@property NSOperationQueuePriority queuePriority;
    • 优先看依赖关系,多个task都ready时才按优先级顺序
  4. 可以很方便的取消一个操作的执行。

    • NSOperation的方法- (void)cancel;
  5. 使用 KVO 观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。、

    @property (readonly, getter=isCancelled) BOOL cancelled;
    @property (readonly, getter=isExecuting) BOOL executing;
    @property (readonly, getter=isFinished) BOOL finished;
    @property (readonly, getter=isReady) BOOL ready;
阅读 169更新于 2019-10-22

推荐阅读
二师兄的博客
用户专栏

个人博客,希望这次能多写点

3 人关注
41 篇文章
专栏主页
目录