iOS多线程编程

作者: 旅行者_sz | 来源:发表于2020-06-29 11:14 被阅读0次

OC中的多线程

  • OC中多线程根据封装程度可以分为三个层次:NSThread、GCD和NSOperation,另外由于OC兼容C语言,因此仍然可以使用C语言的POSIX接口来实现多线程,只需引入相应的头文件:#include <pthread.h>。

一、NSThread

  • 特点:NSThread是封装程度最小最轻量级的,使用更灵活,但要手动管理线程的生命周期、线程同步和线程加锁等,开销较大;

  • 使用方法:NSThread的基本使用比较简单,可以动态创建初始化NSThread对象,对其进行设置然后启动;也可以通过NSThread的静态方法快速创建并启动新线程;此外NSObject基类对象还提供了隐式快速创建NSThread线程的performSelector系列类别扩展工具方法;NSThread还提供了一些静态工具接口来控制当前线程以及获取当前线程的一些信息。

  • 举个🌰:

- (void)viewDidLoad {
    [super viewDidLoad];

    /** NSThread静态工具方法 **/
    /* 1 是否开启了多线程 */
    BOOL isMultiThreaded = [NSThread isMultiThreaded];
    /* 2 获取当前线程 */
    NSThread *currentThread = [NSThread currentThread];
    /* 3 获取主线程 */
    NSThread *mainThread = [NSThread mainThread];
    NSLog(@"main thread");
    /* 4 睡眠当前线程 */
    /* 4.1 线程睡眠5s钟 */
    [NSThread sleepForTimeInterval:5];
    /* 4.2 线程睡眠到指定时间,效果同上 */
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
    /* 5 退出当前线程,注意不要在主线程调用,防止主线程被kill掉 */
    //[NSThread exit];     NSLog(@"main thread");
    
    /** NSThread线程对象基本创建,target为入口函数所在的对象,selector为线程入口函数 **/
    /* 1 线程实例对象创建与设置 */
    NSThread *newThread= [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    /* 设置线程优先级threadPriority(0~1.0),即将被抛弃,将使用qualityOfService代替 */
    newThread.threadPriority = 1.0;
    newThread.qualityOfService = NSQualityOfServiceUserInteractive;
    /* 开启线程 */
    [newThread start];
    /* 2 静态方法快速创建并开启新线程 */
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
    [NSThread detachNewThreadWithBlock:^{
        NSLog(@"block run...");
    }];
    
    /** NSObejct基类隐式创建线程的一些静态工具方法 **/
    /* 1 在当前线程上执行方法,延迟2s */
    [self performSelector:@selector(run) withObject:nil afterDelay:2.0];
    /* 2 在指定线程上执行方法,不等待当前线程 */
    [self performSelector:@selector(run) onThread:newThread withObject:nil waitUntilDone:NO];
    /* 3 后台异步执行函数 */
    [self performSelectorInBackground:@selector(run) withObject:nil];
    /* 4 在主线程上执行函数 */
    [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
}

- (void)run {
    NSLog(@"run...");
}

二、GCD 大中央调度

  • 简介:
    1.虽然GCD 已经出现过一段时间了,但不是每个人都明了其主要内容。这是可以理解的;并发一直很棘手,而 GCD 是基于 C 的 API ,它们就像一组尖锐的棱角戳进 Objective-C 的平滑世界。
    2.GCD(Grand Central Dispatch),又叫大中央调度,对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口,使用更加简单高效,也是苹果推荐的方式。
  • 特点:GCD 是 libdispatch 的市场名称,而 libdispatch 作为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持。它具有以下优点:
    1.GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
    2.GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
    3.GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
  • 术语:
    1.Serial vs. Concurrent 串行 vs. 并发:任务串行执行就是每次只有一个任务被执行,任务并发执行就是在同一时间可以有多个任务被执行。
    2.Synchronous vs. Asynchronous 同步 vs. 异步:GCD多线程编程时经常会使用dispatch_async和dispatch_sync函数往指定队列中添加任务块,区别就是同步和异步。同步指的是阻塞当前线程,要等添加的耗时任务块block完成后,函数才能返回,后面的代码才可以继续执行。如果在主线上,则会发生阻塞,用户会感觉应用不响应,这是要避免的。而有时需要使用同步任务的原因是想保证先后添加的任务要按照编写的逻辑顺序依次执行;异步指的是将任务添加到队列后函数立刻返回,后面的代码不用等待添加的任务完成返回即可继续执行。
    3.Critical Section 临界区:就是一段代码不能被并发执行,也就是,两个线程不能同时执行这段代码。这很常见,因为代码去操作一个共享资源,例如一个变量若能被并发进程访问,那么它很可能会变质(它的值不再可信)。
    4.Race Condition 竞态条件:这种状况是指基于特定序列或时机的事件的软件系统以不受控制的方式运行的行为,例如程序的并发任务执行的确切顺序。竞态条件可导致无法预测的行为,而不能通过代码检查立即发现。
    5.Deadlock 死锁:两个(有时更多)东西——在大多数情况下,是线程——所谓的死锁是指它们都卡住了,并等待对方完成或执行其它操作。第一个不能完成是因为它在等待第二个的完成。但第二个也不能完成,因为它在等待第一个的完成。
    6.Thread Safe 线程安全:线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。一个线程安全代码的例子是 NSDictionary 。你可以在同一时间在多个线程中使用它而不会有问题。另一方面,NSMutableDictionary 就不是线程安全的,应该保证一次只能有一个线程访问它。
    7.Context Switch 上下文切换:一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。
  • Concurrency vs Parallelism 并发与并行:并发代码的不同部分可以“同步”执行。然而,该怎样发生或是否发生都取决于系统。多核设备通过并行来同时执行多个线程;然而,为了使单核设备也能实现这一点,它们必须先运行一个线程,执行一个上下文切换,然后运行另一个线程或进程。这通常发生地足够快以致给我们并发执行地错觉,如下图所示: 并发原理.png
  • 常见的函数调用:
    1.dispatch_sync与dispatch_async:
    直接上代码如下:
   
   /* 1. 提交异步任务 */
   NSLog(@"开始提交异步任务:1");
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      /* 耗时任务... */
      [NSThread sleepForTimeInterval:10];
   });
   NSLog(@"异步任务1提交成功!");
   
   NSLog(@"开始提交异步任务:2");
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         /* 耗时任务... */
         [NSThread sleepForTimeInterval:10];
      });
      NSLog(@"异步任务2提交成功!");
   
   NSLog(@"开始提交异步任务:3");
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         /* 耗时任务... */
         [NSThread sleepForTimeInterval:10];
      });
      NSLog(@"异步任务3提交成功!");
   
      
   /* 2. 提交同步任务 */
   NSLog(@"开始提交同步任务:");
   dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      /* 耗时任务... */
      [NSThread sleepForTimeInterval:10];
   });
   NSLog(@"同步任务提交成功!");

打印结果:

2020-06-29 10:30:59.905203+0800 text_GCD[50668:1734457] 开始提交异步任务:1
2020-06-29 10:30:59.905431+0800 text_GCD[50668:1734457] 异步任务1提交成功!
2020-06-29 10:30:59.905542+0800 text_GCD[50668:1734457] 开始提交异步任务:2
2020-06-29 10:30:59.905660+0800 text_GCD[50668:1734457] 异步任务2提交成功!
2020-06-29 10:30:59.905758+0800 text_GCD[50668:1734457] 开始提交异步任务:3
2020-06-29 10:30:59.905861+0800 text_GCD[50668:1734457] 异步任务3提交成功!
2020-06-29 10:30:59.905965+0800 text_GCD[50668:1734457] 开始提交同步任务:
2020-06-29 10:31:09.907522+0800 text_GCD[50668:1734457] 同步任务提交成功!

通过打印结果的时间可以看出,异步任务提交后立即就执行下一步打印提交成功了,不会阻碍当前线程,提交的任务会在后台去执行;而提交同步任务要等到提交的后台任务结束后才可以继续执行当前线程的下一步。此处在主线程上添加的同步任务就会阻塞主线程,导致后面界面的显示要延迟,影响用户体验。
2.dispatch_queue_t

  • 操作队列主要有两种,并发队列和串行队列,它们的区别上面已经提到,具体创建的方法很简单,要提供两个参数,一个是标记该自定义队列的唯一字符串,另一个是指定串行队列还是并发队列的宏参数。
/* 创建一个并发队列 */
dispatch_queue_t concurrent_queue = dispatch_queue_create("demo.gcd.concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
/* 创建一个串行队列 */
dispatch_queue_t serial_queue = dispatch_queue_create("demo.gcd.serial_queue", DISPATCH_QUEUE_SERIAL);
  • 另外GCD还提供了几个常用的全局队列以及主队列,获取方法如下:
// 获取主队列(在主线程上执行)
dispatch_queue_t main_queue = dispatch_get_main_queue();
// 获取不同优先级的全局队列(优先级从高到低)
dispatch_queue_t queue_high = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue_low = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t queue_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

3.使用 dispatch_after 延后工作
通过该函数可以让要提交的任务在从提交开始后的指定时间后执行,也就是定时延迟执行提交的任务,使用方法很简单.

    // 定义延迟时间:10s
    dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC));
    dispatch_after(delay, dispatch_get_main_queue(), ^{
        // 要执行的任务...
    });

4.让你的单例线程安全dispatch_once_t

+ (instancetype *)sharedInstance {
    static dispatch_once_t once = 0;
    static id sharedInstance = nil;
    dispatch_once(&once, ^{
        // 只实例化一次
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

注:dispatch_once() 以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止;需要记住的是,这只是让访问共享实例线程安全。它绝对没有让类本身线程安全。类中可能还有其它竞态条件,例如任何操纵内部数据的情况。这些需要用其它方式来保证线程安全,例如同步访问数据.
5.dispatch_group_t的使用场景
组调度可以实现等待一组操作都完成后执行后续操作,典型的例子是大图片的下载,例如可以将大图片分成几块同时下载,等各部分都下载完后再后续将图片拼接起来,提高下载的效率。使用方法如下:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*操作1 */ });
dispatch_group_async(group, queue, ^{ /*操作2 */ });
dispatch_group_async(group, queue, ^{ /*操作3 */ }); 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 后续操作... });

6.dispatch_barriers读者写者问题
上图:

解析图.png

三、NSOperation

  • 简介
    NSOperation是基于GCD的一个抽象基类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,但比GCD可控性更强,例如可以加入操作依赖(addDependency)、设置操作队列最大可并发执行的操作个数(setMaxConcurrentOperationCount)、取消操作(cancel)等。NSOperation作为抽象基类不具备封装我们的操作的功能,需要使用两个它的实体子类:NSBlockOperation和NSInvocationOperation,或者继承NSOperation自定义子类。

NSBlockOperation和NSInvocationOperation用法的主要区别是:前者执行指定的方法,后者执行代码块,相对来说后者更加灵活易用。NSOperation操作配置完成后便可调用start函数在当前线程执行,如果要异步执行避免阻塞当前线程则可以加入NSOperationQueue中异步执行。他们的简单用法如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    /* NSInvocationOperation初始化 */
    NSInvocationOperation *invoOpertion = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
    [invoOpertion start];
    
    /* NSBlockOperation初始化 */
    NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSBlockOperation");
    }];
    [blkOperation start];
}

- (void)run {
    NSLog(@"NSInvocationOperation");
}

另外NSBlockOperation可以后续继续添加block执行块,操作启动后会在不同线程并发的执行这些执行快:

/* NSBlockOperation初始化 */
NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"NSBlockOperationA");
}];
[blkOperation addExecutionBlock:^{
    NSLog(@"NSBlockOperationB");
}];
[blkOperation addExecutionBlock:^{
    NSLog(@"NSBlockOperationC");
}];
[blkOperation start];
2017-04-04 11:27:02.805 SingleView[12008:3666657] NSBlockOperationB
2017-04-04 11:27:02.805 SingleView[12008:3666742] NSBlockOperationC
2017-04-04 11:27:02.805 SingleView[12008:3666745] NSBlockOperationA

另外说了NSOperation的可控性比GCD要强,其中一个非常重要的特性是可以设置各操作之间的依赖,即强行规定操作A要在操作B完成之后才能开始执行,成为操作A依赖于操作B:

/* 获取主队列(主线程) */
NSOperationQueue *queue = [NSOperationQueue mainQueue];
/* 创建a、b、c操作 */
NSOperation *c = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"OperationC");
}];
NSOperation *a = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"OperationA");
}];
NSOperation *b = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"OperationB");
}];
/* 添加操作依赖,c依赖于a和b,这样c一定会在a和b完成后才执行,即顺序为:A、B、C */
[c addDependency:a];
[c addDependency:b];
/* 添加操作a、b、c到操作队列queue(特意将c在a和b之前添加) */
[queue addOperation:c];
[queue addOperation:a];
[queue addOperation:b];

NSBlockOperation和NSInvocationOperation可以满足多数情况下的编程需求,如果需求特殊则需要继承NSOperation类自定义子类来更加灵活的实现。

四、多线程中的通信

什么叫做线程间通信

  • 在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信
    线程间通信的体现

  • 1个线程传递数据给另1个线程

  • 在1个线程中执行完特定任务后,转到另1个线程继续执行任务

线程间通信常用方法

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
线程通信示意图

相关文章

网友评论

    本文标题:iOS多线程编程

    本文链接:https://www.haomeiwen.com/subject/psnnfktx.html