iOS知识梳理-多线程2API数理

39次阅读

共计 7897 个字符,预计需要花费 20 分钟才能阅读完成。

在 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;

正文完
 0