iOS底层系列22 -- 多线程基础概念

进程

  • 进程是指在系统中正在运行的一个应用程序;
  • 每个进程之间是相互独立的,每个进程均运行在其专用的且受保护的内存空间内;

线程

  • 线程是进程的基本执行单元,一个进程的所有任务都是在线程中执行的;
  • 进程要想执行任务,必须的有线程,一个进程进程至少要有一条线程;
  • APP应用程序启动会默认开启一条线程,这条线程被称为 主线程 或者 UI线程;

进程与线程之间的关系

  • 进程之间的地址空间是相互独立,不能交叉访问,同一个进程内的线程共享本进程的地址空间;
  • 进程之间的资源是相互独立的,同一个进程内的线程共享本进程的资源;
  • 两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即进程相当于一个工厂,线程相当于工厂中的一条流水线;

线程与RunLoop之间的关系

  • RunLoop与线程是一一对应的,其保存在一个全局的字典当中;
  • RunLoop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务才会被唤醒去执行任务;
  • 对于主线程来说,RunLoop在程序一启动就默认创建好了,在线程结束时被销毁;
  • 对于子线程来说,RunLoop不会默认创建, 其在第一次获取时被创建,所以在子线程用定时器要注意:确保子线程的RunLoop被创建,不然定时器不会回调;

多线程

  • iOS中的多线程同时执行的本质是: CPU在多个任务之间进行快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的假象,让我们认为多个线程在同时执行任务,其中切换的时间间隔就是时间片

多线程的优缺点

优点:
  • 能适当提高应用程序的执行效率;
  • 能适当提高系统资源的利用率,如CPU、内存;
  • 线程上的任务执行完成后,线程会自动销毁;
缺点:
  • 开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB,如果开启大量线程,会占用大量的内存空间,降低程序的性能;
  • 线程越多,CPU在调用线程上的开销就越大;
  • 程序设计更加复杂,比如线程间的通信,多线程的数据共享;

多线程的生命周期

线程的生命周期通常分为5个部分:创建 -- 就绪 -- 运行 -- 阻塞 -- 死亡;其状态之间的切换如下图所示:

Snip20210319_208.png
  • 创建:即实例化线程对象;
  • 就绪:线程对象调用start方法,将线程对象加入可调度线程池,等待CPU的调用,即调用start方法,并不会立即执行,而是进入到就绪状态,需要等待一段时间,经CPU调度后才执行;
  • 运行:CPU负责从可调度线程池中调度线程然后执行,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责,开发人员不能干预;
  • 阻塞:当满足某个预定条件时,可以让线程休眠,即sleep,或者使用同步锁,阻塞线程执行,会将线程从可调度线程池中移除,当线程解除sleep时/获取到锁,会重新将线程加入到可调度线程池中。下面关于休眠的时间设置,都是NSThread的;
  • 死亡:分为两种情况
    • 正常死亡,即线程执行完毕;
    • 非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
  • 简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片),如果时间片用尽,线程就会进入就绪状态队列,如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列;等待事件发生后,线程又会重新进入就绪状态队列;
  • 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列中选择一个线程继续执行;
  • 线程的exit和cancel说明
    • exit:一旦强行终止线程,后续的所有代码都不会执行;
    • cancel:取消当前线程,但是不能取消正在执行的线程;

线程池的工作原理

  • 其工作原理如下图所示:
Snip20210319_210.png
  • 首先线程池有两个重要参数分别为:corePoolSize和maximumPoolSize;
  • corePoolSize表示核心线程池能创建核心线程的最大数量;
  • maximumPoolSize表示线程池能创建线程的最大数量;核心线程池包含在线程池中。
  • 【第一步】:当有任务提交过来,首先判断核心线程池是否已满(corePoolSize)
    • 未满,创建核心线程执行任务;
    • 已满,进入第二步;
  • 【第二步】:判断工作队列是否已满
    • 未满,将任务添加到工作队列中;
    • 已满,进入第三步;
  • 【第三步】:判断线程池是否已满(maximumPoolSize)
    • 未满:创建非核心线程执行任务;
    • 已满:进入第四步;
  • 【第四步】:执行饱和策略,
  • 通常有以下四种饱和策略:
    • AbortPolicy(抛出一个异常,默认的)
    • DiscardPolicy(新提交的任务直接被抛弃)
    • DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
    • CallerRunsPolicy(交给线程池调用所在的线程进行处理,即将某些任务回退到调用者)

iOS中多线程的实现方案

  • 主要有四种分别为:pthreadNSThreadGCDNSOperation
Snip20210319_211.png
  • 下面通过代码案例分别演示这四种多线程方案的实现:
【pthread】
//线程回调函数
void * pthreadTest(){
    NSLog(@"===>%@", [NSThread currentThread]);
    return NULL;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    pthread_t threadId = NULL;
    //c字符串
    char *cString = "HelloCode";
    //创建一个字线程
    int result = pthread_create(&threadId,NULL,pthreadTest,cString);
    if (result == 0) {
        NSLog(@"pthread 创建成功");
    }else{
        NSLog(@"pthread 创建失败");
    }
}
  • 需导入#import
Snip20210319_212.png
【NSThread】
Snip20210319_214.png
【GCD】
Snip20210319_215.png
【NSOperation】
Snip20210319_217.png

线程安全问题

  • 同一时刻多条子线程共同访问共享资源数据,容易引发数据错乱和数据安全问题,有以下两种解决方案:
    • 互斥锁(即同步锁):@synchronized
    • 自旋锁;
互斥锁
  • 用于保护临界区,确保同一时间,只有一条线程能够访问执行;
  • 加了互斥锁的代码,当新线程访问时,如果发现有其他线程正在访问共享资源,新线程就会进入休眠状态;
  • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差;
  • 能够加锁的任意的NSObject对象;
  • 锁对象一定要保证所有的线程都能够访问;
自旋锁
  • 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态;
  • 锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本身就有一把自旋锁;
  • 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能;
【面试题】:自旋锁 vs 互斥锁
  • 相同点:在同一时间,保证了只有一条线程访问共享资源;
  • 不同点:
    • 互斥锁:发现其他线程执行,当前线程 休眠(即就绪状态),进入等待执行,即挂起,一直等其他线程打开之后,然后唤醒执行;
    • 自旋锁:发现其他线程执行,当前线程一直询问(即一直访问),处于忙等状态,耗费的性能比较高;
  • 场景:根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理
    当前的任务状态比较短小精悍时,用自旋锁,反之用互斥锁;

你可能感兴趣的:(iOS底层系列22 -- 多线程基础概念)