iOS多线程编程介绍

作者: THU_Yu | 来源:发表于2020-08-26 18:45 被阅读0次

一、进程与线程

   进程是系统中正在运行的应用程序,是CPU分配资源和调度的单位,线程是CPU调度(执行任务)的最小单位,一个进程内必须至少包含一个线程。
   不同进程拥有不同的内存资源,而在同一进程的不同线程则是共享进程的资源(这也就导致可能出现线程安全问题)。

二、多线程编程

   为什么需要使用多线程?在一个APP的项目中,用户所看到的UI界面都是在主线程进行改动,如果我们在主线程的某一个方法中执行了一项耗时操作(比如大量for循环、大批量网络请求等),由于同一线程内的程序是串行顺序执行,因此会导致主线程堵塞,界面卡死。如果我们将这些耗时操作放到一个新的线程中处理,透过线程的并行执行,可以避免主线程阻塞,等到子线程处理完成后,可以利用线程间的通信方法通知主线程。
   线程的运行方式有两种:线程同步线程异步。线程同步是指线程按顺序执行,通常发生在不同线程需要访问一个加锁的数据时;线程异步就是线程并行,通过CPU的快速调度,实现同时运行的效果。

  • iOS中线程的实现方案

技术方案 简介 语言 线程生命周期
pthread 通用的多线程API
适用于Unix\Linux\Windows等系统
跨平台\可移植
使用难度大
C 程序员管理
NSThread 面向对象
简单易用,可以直接操作线程对象
OC 程序员管理
GCD 为了替代NSThread,更底层
有效利用系统多核
C 自动管理
NSOperation 基于GCD
面向对象
比GCD多了一些简单易用的功能
OC 自动管理
  • 线程的状态

   线程可以分为几个状态:新建、就绪、运行、阻塞、死亡。新建线程的时候,会在内存中创建一个线程,这时的线程还不可以被调度。当进入就绪阶段后,线程会被放入可调度线程池,接受cpu的调度。当cpu调度时,线程进入运行阶段。在运行阶段调用sleep或等待同步锁时,会进入阻塞状态,此时线程被移出可调度线程池,直到阻塞条件结束回到就绪状态。当线程内的方法执行结束后或其他异常导致强制退出时,进入死亡状态,进行线程的销毁。

  • 线程安全

   线程安全问题是指当不同线程同时对一个数据进行请求并修改时,会造成数据处理错误的情况。下面一个存钱取钱的例子便是一个线程安全问题:

   现在有两个人(线程):A(线程A)和B(线程B),当A和B在不同的地方同时向银行卡(进程的内存资源)请求余额信息(数据),此时银行卡会告诉他们余额是1000元,接着A先进行了存钱操作,并修改银行卡余额为2000(修改数据),但对于B而言,B所知道的银行卡余额是1000元,此时B再进行取钱操作,并修改银行卡余额为500元,这样银行卡的余额会被覆盖,变成只有500元,很明显得到了一个错误的数据。
   为了解决上面提到的数据错误问题,我们需要对数据上锁,当一个线程要访问某个数据且进行修改时,把这个数据锁住,等到该线程修改完毕再解锁让其他线程取用,这个就是互斥锁

   从上面的流程来看,互斥锁使得这些线程按照访问顺序执行,也就是前面提到的线程同步。下面我们用代码实现一个互斥锁(使用NSThread)。
ViewController.h

#import <UIKit/UIKit.h>

@interface ViewController:UIViewController
@end

ViewController.m

#import  "ViewController.h"

@interface ViewController:UIViewController
@property (nonatomic, strong)NSThread *thread1;
@property (nonatomic, strong)NSThread *thread2;
@property (nonatomic, strong)NSThread *thread3;
@property (nonatomic, assign)NSInteger num;
@end

@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    // 设置初始数据
    self.num = 100;
    // 开辟多线程,三个线程都执行func,func内会对self.num进行修改,因而产生线程安全问题
    self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
    self.thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
    self.thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];
    // 设置线程名称
    self.thread1.name = @"线程A";
    self.thread2.name = @"线程B";
    self.thread3.name = @"线程C";
    // 启动线程
    [self.thread1 start];
    [self.thread2 start];
    [self.thread3 start];
}

- (void)func
{
    while (1)
    {
        @synchronize(self)  // 加上互斥锁,self是锁对象(要使用全局的对象),通常用self即可
        {
            NSInteger count = self.num;
            if (count > 0) {
                self.num = count - 1;
                for (int i; i < 1000000; i ++) // 耗时操作
                {}
                // 打印当前线程名称和num
                NSLog(@"%@-----%zd", [NSThread currentThread].name, self.num);
            } else {
                // 打印当前线程名称和num`
                NSLog(@"%@-----%zd", [NSThread currentThread].name, self.num);
                break;
            }
        }
    }
}
@end
  • NSThread

   线程创建

/*
* 参数说明:
*   第一个参数:目标对象
*   第二个参数:方法选择器(希望线程执行的方法)
*   第三个参数:前一个参数(方法)中要传入参数
* 特点:需要启动线程,可以获得线程对象进行详细设置
*/
NSThread *thread = [NSThread alloc] initWithTarget:self selector:@selector(func)  object:nil];

// 直接分离出一条子线程
/*
* 参数说明:
*   第一个参数:方法选择器(希望线程执行的方法)
*   第二个参数:目标对象
*   第三个参数:第一个参数(方法)中要传入参数
* 特点:不需要启动线程,无法得到线程对象进行详细设置
*/
[NSThread detachNewThreadSelector:@selector(func) toTarget:self withObject:nil];

// 开启后台线程
/*
* 参数说明:
*   第一个参数:方法选择器(希望线程执行的方法)
*   第二个参数:第一个参数(方法)中要传入参数
* 特点:不需要启动线程,无法得到线程对象进行详细设置
*/
[self performSelectorInBackground:@selector(func) withObject:nil];

   线程启动

[thread start];

   得到主线程和当前线程

// 得到主线程
NSThread *mainThread = [NSThread mainThread];
// 得到当前线程
NSThread *currentThread = [NSThread currentThread];

// 判断线程是否为主线程
/*
* 1.取得当前线程的number,主线程的number == 1
* 2.透过isMainThread方法
*/
// 判断number == 1
[NSThread currentThread].number == 1;
// isMainThread方法
[thread isMainThread];

   设置线程名称

thread.name = @"线程1";

   设置线程优先级

// 优先级是介于0~1的数,0代表低优先级,1代表高优先级,默认是0.5
// 优先级也意味着cpu调用线程的概率,优先级越高,cpu调用的概率就越大
thread.threadPriority = 1;

   线程通信

// 子线程向主线程通信
/*
* 参数说明:
*   第一个参数:方法选择器(回到主线程要执行什么方法)
*   第二个参数:前一个参数(方法)中要传递的参数
*   第三个参数:是否等待该方法执行结束才继续往下执行,YES代表等待
*/
[self performSelectorOnMainThread:@selector(func) withObject:nil  waitUntilDone:YES];

// 两个线程间通信(不限主线程)
/*
* 参数说明:
*   第一个参数:方法选择器(切换到新线程要执行什么方法)
*   第二个参数:想要回到的线程
*   第三个参数:前一个参数(方法)中要传递的参数
*   第四个参数:是否等待该方法执行结束才继续往下执行,YES代表等待
*/
[self performSelector:@selector(func) onThread:[NSThread  mainThread] withObject:nil  waitUntilDone:YES];

   使用NSThread创建的线程,其生命周期仅限于执行的方法,当方法执行结束后,该线程会被释放。

  • GCD

   GCD的核心概念
      1. 任务:要做什么
      2. 队列:存放任务
   使用步骤
      1. 创建队列

// 并发队列
/*
* 参数说明:
*   第一个参数:队列的名字,C语言字符串
*   第二个参数:队列类型,这里是并发队列
* 自动开启多线程,同时执行任务
* 开多少条线程不是由任务的数量决定,是GCD内部自己决定的
* 仅在异步函数才有效
*/
dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);

//全局并发队列
/*
* 参数说明:
*   第一个参数:优先级,一般传入默认的优先级即可
*   第二个参数:未来接口预留参数,现在传0即可
*/
dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 串行队列
/*
* 参数说明:
*   第一个参数:队列的名字,C语言字符串
*   第二个参数:队列类型,这里是串行队列,默认是串行,如果写NULL也是串行
* 任务必须一个接着一个执行
*/
dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);

// 主队列,特殊的串行队列
/*
* 与主线程相关联,凡是放在主队列里的任务都要在主线程中串行执行
* 当同步函数和主队列一起使用时会发生死锁,因为同步函数要等到dispatch_sync函数执行结束才往下运行,而被压进主队列的任务有会马上被拿出来给主线程执行,此时主线程就会出现死锁的情况。
*/
dispatch_queue_t  queue = dispatch_get_main_queue();

      2. 封装任务

// 使用函数来封装任务
// 同步
/*
* 参数说明:
*   第一个参数:队列
*   第二个参数:想要封装的任务
* 只能在当前线程中执行任务,不具备开启新线程的能力
* 必须等待当前任务完成,才能执行下面的任务
*/
dispatch_sync(dispatch_queue_t  queue, dispatch_block_t block);

// 异步
/*
* 参数说明:
*   第一个参数:队列
*   第二个参数:想要封装的任务
* 可以在新的线程中执行任务,具备开启新线程的能力
* 不必等待当前任务完成,可以直接执行下面的任务
*/
dispatch_async(dispatch_queue_t  queue, dispatch_block_t block);

   线程间通信(透过嵌套执行)

dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // 在子线程进行某些操作
    NSLog(@"%@",[NSThread currentThread]);
    ...
    dispatch_async(dispatch_get_main_queue(), ^{
       // 传递数据到主线程处理
       ...
       NSLog(@"%@",[NSThread currentThread]);
    });
});

   其他常用函数

// 一次性代码:整个程序运行过程中只会执行一次,线程安全的(内部已加锁)
// 可以应用在单例模式
// 实现原理:透过判断onceToken的值来决定是否执行,onceToken==0代表没有执行过
static  dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"Once----");
});

// 延迟执行
/*
* 参数说明:
*   第一个参数:设置延迟时间(GCD的时间单位是ns)
*   第二个参数:队列(决定任务在哪个队列执行)
*   第三个参数:设置任务
*/
// 实现原理:先等两秒,再将任务放到队列
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"%@", [NSThread currentThread]);
});

// 快速迭代
/*
* 参数说明:
*   第一个参数:遍历的次数
*   第二个参数:队列(不可以使用主队列,会发生死锁;如果使用普通的串行队列,则只会在主线程执行)
*   第三个参数:设置任务,这个block需要接受一个参数,类似于for循环的int i
* 会开启多条子线程和主线程并发的执行任务。
*/
dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t i){
    NSLog(@"%zd-----%@", i, [NSThread currentThread]);
});

// 栅栏函数
// 由于异步执行时的顺序不能保证,有时候我们希望先执行某些异步操作后,先执行某个任务再执行后续任务,这时候需要栅栏函数来进行拦截。
// 下面的代码段能够保证先并发执行打印1、2(1、2顺序不保证),然后打印stop,再并发执行打印3、4(3、4顺序不保证)
// 不能使用全局并发队列(不能拦截)
dispatch_queue_t  queue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    NSLog(@"1-------%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"2-------%@",[NSThread currentThread]);
});
dispatch_barrier_async(queue, ^{
    NSLog(@"---stop---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"3-------%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
    NSLog(@"4-------%@",[NSThread currentThread]);
});

   队列组

// 使用队列组可以监听任务的执行情况
// 下面代码实现所有打印数字结束后(打印数字的任务是并发执行,顺序不保证),再打印stop
// 创建队列组和队列
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue1 = dispatch_queue_create("test1.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("test2.queue", DISPATCH_QUEUE_CONCURRENT);

// 封装任务,添加到队列并监听任务的执行情况
// dispatch_group_async函数是对dispatch_group_enter、dispatch_async、dispatch_group_leave三个函数的封装,需要注意的是dispatch_group_leave必须写在dispatch_async的block中,具体封装如下:
//dispatch_group_enter(group);
//dispatch_async(queue1, ^{
//    NSLog(@"1-------%@",[NSThread currentThread]);
//    dispatch_group_leave(group);
//});
dispatch_group_async(group, queue1, ^{
    NSLog(@"1-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue1, ^{
    NSLog(@"2-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue1, ^{
    NSLog(@"3-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue2, ^{
    NSLog(@"4-------%@",[NSThread currentThread]);
});
dispatch_group_async(group, queue2, ^{
    NSLog(@"5-------%@",[NSThread currentThread]);
});

// 拦截通知,等待所有任务执行完毕才执行,这个函数的queue1只是决定block里任务放在哪个队列中
// dispatch_group_notify内部是异步执行
dispatch_group_notify(group, queue1, ^{
    NSLog(@"---stop---%@",[NSThread currentThread]);
});
  • NSOperation

   NSOperation的核心概念
      1. NSOperation:操作,抽象类,只能将操作封装到其子类中。
      2. NSOperationQueue:队列
   使用步骤
      1. 创建队列

// 自定义队列:并发队列,可以设定成串行队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 主队列:串行队列,和主线程相关
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

      2. 将要执行的操作封装到一个NSOperation对象中

// NSInvocationOperation
// 封装操作对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(func)  object:nil];
// 执行操作,压入队列不需要执行操作,由队列自动调用,如果直接执行会在当前线程执行,不会开启新的线程
[operation start];

// NSBlockOperation
// 封装操作对象
NSBlockOperation *operation = [NSBlockOperation blockWithOperation:^{
    NSLog(@"1-----%@", [NSThread currentThread]);
}];
// 追加任务,当一个操作对象中的任务数量大于1的时候,会开启新的线程来执行`
[operation addExecutionBlock:^{
    NSLog(@"2----%@", [NSThread currentThread]);
}];

// 执行操作,压入队列不需要执行操作,由队列自动调用,如果直接执行会在当前线程执行,不会开启新的线程
[operation start];

      3. 将NSOperation对象添加到NSOperationQueue中

[queue addOperation:operation];
// 使用NSBlockOperation有较为简便的写法,不需要前面的封装操作
// 实现原理:先将block封装成NSBlockOperation,再将NSBlockOperation压入队列
[queue addObjectWithBlock:^{
    NSLog(@"1-----%@", [NSThread currentThread]);
}];

         设置队列的最大并发数量

// num为数字,当设置成1的时候相当于串行队列,但线程数可能不为1,只是代表同时只有一个线程在运行
// 默认为-1,代表系统认为要开多少就开多少,不受限制
// 设置最大并发数==1时,只能控制单任务的操作顺序执行,如果一个操作中有追加的任务,会不受最大并发数的影响
queue.maxConcurrentOperationCount = num;

         队列的挂起和取消

// 暂停队列,要等到当前操作结束才能暂停,当前操作不可分割
[queue setSuspended:YES];
// 恢复队列
[queue setSuspended:NO];
// 取消队列,只能取消队列中等待执行的操作,正在执行的操作不能中断
// 如果想要取消当前操作,可以在操作中判断isCancelled属性,因为cancelAllOperations方法会修改所有队列中操作的的isCancelled属性
[queue cancelAllOperations];

         自定义NSOperation子类

// 继承自NSOperation
// 重写main方法
// 使用的时候可以直接用alloc
// 好处:可以复用大量操作
// 需要注意的是,当main中执行了一段耗时任务时,建议判断一下isCancelled属性
- (void)main
{
    NSLog(@"main----%@", [NSThread currentThread]);
}

         操作队列的依赖和监听

// operation1依赖于operation2,代表operation2先于operation1执行
// 设置依赖必须在添加到队列前设置
// 不能设置循环依赖
// 可以设置跨队列依赖
[operation1 addDependency:operation2];

// 监听任务执行完毕
operation.completionBlock = ^{
    NSLog(@"---Finish---");
};

         线程中通信

NSBlockOperation *operation = [NSBlockOperation blockWithOperation:^{
    // 执行某些任务
    NSLog(@"1-----%@", [NSThread currentThread]);
    ...
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // 主线程内执行某些任务`
        NSLog(@"main-----%@", [NSThread currentThread]);
        ...
    }];
}];

// 把操作添加到队列
[queue addOperation:operation];
  • GCD和NSOperation的比较

  1. GCD是C语言的API,NSOperation是Objective-C的对象。
  2. 在GCD中,任务用block封装,是一个轻量级的数据结构;NSOperation的操作用NSOperation封装,是一个重量级的数据结构。
  3. NSOperationQueue可以取消操作,GCD则无法。
  4. NSOperation可以指定依赖关系。
  5. NSOperation可以通过KVO对NSOperation对象进行控制。
  6. NSOperation可以制定操作的优先级。
  7. 可以自定义NSOperation来实现操作复用。

相关文章

  • iOS POSIX多线程编程

    关于多线程的介绍、多线程的创建、使用场景和Runloop可以参考《iOS多线程编程指南》。已上传到GitHub仓库...

  • FLAnimatedImageView加载卡顿问题。

    GCD 的调用 转载“iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用”

  • iOS 多线程编程 教程收录

    iOS多线程编程

  • 线程

    iOS 多线程:『GCD』详尽总结 NSThread详解 IOS 多线程编程 『NSOperation、NSOpe...

  • iOS多线程编程介绍

    一、进程与线程    进程是系统中正在运行的应用程序,是CPU分配资源和调度的单位,线程是CPU调度(执行任务)的...

  • iOS多线程04-NSOperation实践

    推荐文章 iOS多线程01-介绍iOS多线程02-NSThread实践iOS多线程03-GCD实践iOS多线程04...

  • ios 多线程

    通过这篇文章,再熟悉一下多线程,这里主要是根据自己的理解,来介绍一下多线程 iOS有三种多线程编程的技术,分别是:...

  • iOS------GCD一些概念的整理

    iOS------GCD一些概念的整理 在iOS开发中,经常会使用到GCD多线程编程的技术,在这里我们不多介绍GC...

  • ***线程,GCD,runloop(2)

    第三篇:多线程编程的多种方式 iOS执行多线程编程常用的有以下几种方式 NSThread GCD NSOperat...

  • iOS多线程编程(三) NSThread

    多线程系列篇章计划内容:iOS多线程编程(一) 多线程基础[https://juejin.im/post/6890...

网友评论

    本文标题:iOS多线程编程介绍

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