zoukankan      html  css  js  c++  java
  • iOS多线程理论知识过一遍

    一、多线程知识的简单介绍

    1、进程、线程、任务

    1.1、什么是进程

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

    比如同时打开酷狗、迅雷两款软件,系统就会分别启动2个进程。

     

     通过电脑上的“活动监视器”可以查看Mac系统中所开启的进程。

     

    在苹果手机上双击home键也可以看到系统中正在运行的进程。

     

    1.2、什么是线程

      1个进程要想执行任务,必须要有线程(每个进程至少要有1条进程)。线程是进程的基本执行单元,1个进程(应用程序)的所有任务都是在线程中执行的,比如使用酷狗播放音乐、使用迅雷下载电影,都需要在线程中执行。

     

     

    1.3、什么是任务

      应用程序中需要做的事情都抽象为任务。比如酷狗播放音乐的任务、迅雷下载文件A的任务、迅雷下载文件B的任务。

    任务需要放置在线程中执行,可以放置多个任务到一个线程中,但是1个线程中任务的执行是串行的。串行的意思就是说,如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,1个线程只能执行1个任务。

    比如在迅雷进程中的线程2中,有3个下载任务(分别下载文件A、文件B、文件C),因此也可以认为线程是进程中的1条执行路径。

     

    2、多线程

      综上,一个应用程序就是一个进程,进程中想要执行任务就需要创建线程,然后把任务放置在线程中执行,一个进程至少有一个线程,一个线程中可以放置有多个任务,但是同一个线程中的任务的执行是串行的,是一个接一个执行的。 

    CPU会对线程进行调度,CPU调度到哪个线程,就会执行该线程中的任务。同一时间,CPU只能处理1条线程,也就是说,只有1条线程在工作(执行任务)。1个进程中可以创建开启多条线程,每条线程中放置不同的任务,如果CPU调用线程的时间足够快,也就是说CPU快速地在多条线程之间切换调度,那就造成了多个线程并发执行的假象,也就是感觉多个文件在同时下载,这是创建多个线程的好处之一。 

         

     

    创建多个线程的第二个好处就是,将比较耗时的任务放到其他线程中,不影响主线程中任务的执行。

    上面说到过,一个进程中至少有一个线程,其实这个线程称之为主线程或UI线程,当一个iOS程序运行后,这个程序就是一个进程,在这个进程中默认会开启1条线程,这个至少存在的主线程的主要作用就是显示或者刷新UI界面、处理UI事件(比如点击事件、滚动事件、拖拽事件等)。因为用户使用手机上的软件,手指会在屏幕上频繁的做很多操作,因此主线程要做的事情其实很多的,刚刚上面提到了,在同一个线程中的任务是串行执行的,只有当上一个任务执行完成之后,才会执行下一个任务,如果将其他耗时的任务放置在主线程中,那就会让接下来的UI事件处理不及时,给用户很“卡顿”的感觉。解决这种卡UI线程的问题的方式就是再创建一个线程,将这种耗时的任务放置在其他线程中。

     

    综上所述,使用多线程的情景是这样的,首先如果出现比较耗时的任务操作,那么不能将这类任务放置在主线程中,需要另外创建开启额外的线程,这种额外的线程,称之为“子线程”。然后如果这类耗时任务比较多的情况下,与其将它们统统放置在同一个子线程中,让这些耗时任务一个接一个被执行,还不如创建开启多个子线程,每个子线程中只放置一个耗时任务,当CPU快速地在多个线程之间切换调度时,会发现多个耗时任务同时在执行,可以提高程序的执行效率。既然如此,是不是子线程开得越多越好?秉持“一个子线程中就一个任务”的做法呢?显然不行。

    开启多个子线程,即多线程的好处:(1)能适当提高程序的执行效率;(2)能适当提高资源利用率(CPU、内存利用率)。

    开启多个子线程,即多线程的缺点:(1)开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能。(2)线程越多,CPU在调度线程上的开销就越大,CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源。(3)进程中开启的线程太多了,每个线程被调度执行的频次就会降低,线程的执行效率也会降低,包括主线程被调度的频率也会降低,也相当于是造成了主线程的卡顿。(4)多线程会涉及到线程之间的通信、线程之间的数据共享,如果子线程开启太多,程序设计会更加复杂。

    综上所述,有三种情况会造成UI主线程的卡顿,即用户使用软件感觉很卡、很不爽,原因:

    (1)把一些耗时的任务,放在了UI主线程中

    (2)开启的子线程太多了,导致UI主线程被CPU调度的频率下降了

    (3)CPU、内存这些配置太低了,就算不做耗时操作,频繁的操作UI界面,都让CPU够呛了

     

    二、后续知识的展望

     

    打开一个应用程序就相当于是开启了一个进程,进程开启后默认开启了一个主线程用来处理UI事件,接下来为了处理耗时操作,启用了多个子线程来完成任务。为了实际场景的需要,自然而然需要想到以下问题:

    (1)既然线程的创建是为了完成任务完成某项操作,任务总有完成的时候,那么线程自身的生命周期是什么的呢?尤其是子线程。

    (2)就算子线程之间可以无优先级差别,但是主线程被CPU调度的优先级至少要比子线程要高吧?线程的优先级如何保证?

    (3)子线程的创建是为了完成某项任务,任务执行过程中或者是执行完之后,至少要“通知”主线程吧?这就涉及到线程之间的通信问题。

    (4)既然多线程技术,能够让多条线程并发执行,就有可能两条及以上线程同时访问同一个数据,由于从“数据的读取”、“数据的操作”到“数据的写入”整个过程需要一定的时间,如果线程A读取数据后,更新数据之前,线程B又读取了同一块数据,那么就会造成数据紊乱。这个问题涉及到数据的安全问题,多线程对数据的安全造成影响。

    (5)为了得到多线程的高效性,我们面对多个耗时操作时,我们会创建多个线程将任务放置进去。但是任务之间有存在“顺序性”、“依赖性”、“衔接性”、“监听性”等关联。线程层面有优先级的设置,但是没必要将线程之间设置顺序和依赖等关系,必须要等到线程A中的任务执行完之后,在执行线程B中的内容,这样的做法没有合理性。因为如果两个线程有这种依赖顺序性,完全可以等线程A中的任务执行完之后再把原本线程B中的任务,放置在线程A中执行,何必先创建线程B然后让它一直等着?再说了,线程这个概念,更像是“通道”的设计理念。所以任务之间的衔接顺序性,还是要在“任务”这个层面进行设计解决。

     

    三、线程的生命周期(线程的状态)

    线程的生命周期包括:线程的创建、线程的开启、线程的运行、线程的阻塞、线程的销毁

     1、线程的创建

    线程的存在就是为了完成某项任务,某项操作,一个线程中可以放置多个任务,当然同一个线程中的任务的执行是一个接一个的,也就是先进先出原则。

    创建线程的时候,可以直接把任务作为一个参数就扔进去了;也可以先创建一个线程,然后在把任务扔进去,都行。

     

    2、线程的开启

    线程创建以后,到任务的执行,中间还有一个线程状态,那就是要开启线程,毕竟需要先布好局然后在启动。线程开启后意味着这条线程接受CPU的调度,但是线程中的任务是否此时正在执行,取决于CPU此时是否正在调度该条线程。线程的开启,其实就是将该线程放入“可调度线程池”中。

     

    3、线程的运行

    当CPU调度该条线程时,线程中的任务执行,此时就定义为该线程处于运行状态。

     

    4、线程的阻塞

    线程的阻塞就意味着,该条线程首先没有销毁,但是又处于不被CPU可调度的状态。因此,线程的阻塞的相反面,并非线程的运行,而是线程的开启。这里补充一个概念叫做“可调度线程池”,CPU会高速不断切换调度“可调度线程池”中的线程。为了保证CPU的高速切换调度,因此不能额外增加CPU的负担,因此将“暂时不被安排调度”的线程,直接从“可调度线程池”中移除,这样的设计,就可以让CPU在“可调度线程池”中高速完成切换调度工作。

    线程的阻塞如何发生呢?一般是对线程调用了sleep方法,或者是线程在等待同步锁。后面会说明这几个概念。

     

    为了便于理解,也可以用下面这张图理解“可调度线程池”:

     

     

     

     

    5、线程的销毁

    前面已经说过,线程的存在就是为了完成某项任务某项操作,因此线程中任务的执行完成,就会让线程也“自然销毁掉”。实际中,线程的销毁有三种可能:当线程中的任务执行结束、线程发生异常、线程被强制退出。

    线程销毁后,线程对象就从内存中移除掉了,更别说还存在“可调度线程池”中。

     

    综上,线程所处的各种状态,可以用下面这张图表示出来:

     

     

     

    四、线程的优先级

    可以对线程被调度的优先级进行设置,调度优先级的取值范围是0.0~1.0,默认0.5,值越大,优先级越高。

    “高优先级线程”被分配CPU的概率高于“低优先级线程”,但无论是级别相同还是不同,线程调度都不会绝对按照优先级执行,每次执行结果都不一样,调度算法无规律可循,所以线程之间不能有先后依赖关系。

    如果对“线程优先级”的说法在通俗一点就是,优先级高度只是决定了被调用的概率更高,但绝不是先调用高优先级的线程,再调用低优先级的线程,不是这样的。

     

    五、线程的通信

    在一个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信。线程通信的体现或者说场景:(1)1个线程传递数据给另一个线程(2)在1个线程中执行完特定任务后,“通知”另一个线程执行另一个特定任务。

    具体的场景,比如说,手机屏幕上需要显示2张图片,图片需要从网络上下载,当其中一张图片下载完成后,不用等另一张也下载好,就可以先在主线程中(之前说过,主线程主要用来处理UI刷新等)显示出该张图片。这个过程就涉及到线程之间的通信。

     

    六、多线程的线程安全

    所谓的线程安全,本质原因就是从“数据的读取”、“数据的操作”到“数据的写入”整个过程需要一定的时间,但是在这段时间内,又会有其他线程中的程序同样的对这块数据进行了“读取、操作、写入”,导致这块数据的真实值出现了混乱。这种线程问题,在多个线程访问同一对象、同一变量、同一个文件等情况,都会注意线程安全问题。

    这类典型例子就是“买票卖票系统”、“存钱取钱系统”,两个例子的逻辑是一样的,图示如下:

     

     

    线程读取写入的安全问题,很容易理解,如何防止这类数据混乱呢?一种很简单的处理思路就是,当一条线程对该变量进行读取的时候开始,就不允许其他线程读取该变量,直到第一条线程对该变量的处理完成后(即完成“读取”-“操作”-“写入”),再允许其他线程对该变量的读取及写入操作。

    线程安全问题的图示如下:

     

     解决线程安全问题的方式方法图示如下:

     

    解决多线程的线程安全问题的设计思路有了,如果实现呢?

    结合语言特性,有两种方式可以使用:(1)利用互斥锁将“对变量的读取-处理-写入”代码锁定(2)直接将该变量对应的属性用原子属性限定符。

    1、利用互斥锁将“对变量的读取-处理-写入”代码锁定

    线程安全问题是因为多个线程对同一个变量进行了读取写入操作,因此这多个线程都包含有“对变量的读取-处理-写入”的代码,如果将这段相同的代码抽离出来,并且利用语言特性,将该段代码“加锁”,这段代码没有执行完全之前,不能再次被调用。这样就能保证先入为主的线程对该变量的读取及写入操作整个过程中,没有其他代码去读取该变量了。

    互斥锁的使用格式:

    @synchronized(锁对象){

      //需要锁定的代码

    }

    注意:锁定1份代码只用1把锁,用多把锁是无效的。

    代码示例就是:

    @synchronized(self) {
        //1.先检查票数
        int count=self.leftTicketsCount;
        if (count>0) {
            //2.票数-1 
            self.leftTicketsCount= count-1;
        } 
    }    

    互斥锁的优缺点:

    优点:能有效防止因多线程抢夺资源造成的数据安全问题

    缺点:需要消耗大量的CPU资源

    附加知识:互斥锁与线程同步的关系(百度搜索)

     

    2、直接将该变量对应的属性用原子属性限定符

    OC在定义属性时有nonatomic和atomic(默认)两种选择,nonatomic是原子属性,会为setter方法加锁;atomic是非原子属性,不会为setter方法加锁。

    atomic加锁原理如下:

    @property (assign, atomic) int age;
    
    - (void)setAge:(int)age
    {
    
        @synchronized(self) {
           _age = age;
        }
    }

    原子属性和非原子属性的对比:

    atomic:非原子属性,线程安全,需要消耗大量的资源

    nonatomic:原子属性,非线程安全,适合内存小的移动设备

    3、解决线程安全问题的最可靠的方式是:

    所有属性依然都声明为nonatomic、

    尽量避免多线程抢夺同一块资源、

    尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减少移动开发端的压力,因为在业务复杂的情况下,完美的解决好线程安全问题,并不简单。

     

    七、任务的依赖顺序等问题

    上面的内容基本上把引进多线程技术相关的内容都简单介绍了一遍,本来是只准备把任务的依赖顺序等问题汇总出来,但是为了后面内容的学习,我还是决定把实际场景中遇到的实际需求都列出来,列出这些问题后可以先思考下按照现在所掌握的知识,看看怎么去解决。其实这些问题,苹果公司都是推出了框架来解决的,比如NSThread、GCD、NSOperation。但是呢,我不太建议直接跳过我现在整理出的这些场景,然后直接学习NSThread、GCD、NSOperation,因为像GCD、NSOperation这些库,都是有自己的一套架构思路,在学习它们之前,如果不清楚了罗列下多线程使用的各种场景会遇到的实际需求,就直接学习它们,会有一种被牵着思路走的感觉,so,我还是尽量把这部分的需求罗列清楚。

     

    八、多线程技术的“三大主题”

    在iOS开发中 ,关于多线程现有的框架有:pthread、NSTread、GCD、NSOperation,这个框架的引入只是为了更好的操作这“三大主题”,因此理解梳理好这“三大主题”的基本内容是后面理解4个框架的基础。可以将“任务”看作是目标,“进程”和“线程”看作是实现的途径。具体不展开写作,看图示意吧。

     

     

     

     

    九、GCD的介绍和使用

     

    全称是Grand Central Dispatch,翻译为“强大的中央调度器”,也可幽默翻译为“牛逼的中枢调度器”。

     

    是苹果公司提出的解决方案,是纯C语言,框架接口中提供了非常多强大的函数。

    GCD框架存在于libdispatch.dylib这个库中,这个动态库包含了GCD的所有内容,任何iOS程序,默认都加载了这个库,在程序运行过程中会动态的加载这个库,开发人员不需要手动导入

     

    GCD对于“三大主题”的态度是怎样的呢,如下图所示:

     

     

    也就是说,关于多核的应用,因为会涉及到偏硬件层面的逻辑判断,为了iOS开发人员专心写业务代码,GCD将这部分的逻辑判断和相关的代码执行全部隐藏起来,框架内部根据当下的情景需要等因素,自行判断并执行相关代码,比如究竟开多少条线程来处理当前的众多任务,GCD内部有自己的一套判断逻辑。相似的地方还有, 线程被调度的优先级也不用开发人员自己关心,线程的生命周期的管理也交给GCD自行管理。

    关于线程间的通信,GCD暴露出block回调参数的方式暴露接口,由开发人员使用自己的逻辑。

    多线程的数据读写安全,和任务的取消、暂停与恢复,这类事务,由开发人员自行想办法操作。

    任务的依赖关系上,GCD引入了几个概念体系来操作,两个函数(同步函数、异步函数),两种队列(串行队列、并发队列)。

    因此开发人员把这两个函数和两种队列弄清楚,就可以大致把握好了GCD的使用。

    1、GCD中有两种函数(同步函数、异步函数)用来执行任务

    1)同步函数----在当前线程中执行任务

    dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

    参数说明:queue--队列,block--任务

     

    2)异步函数----在另一条线程中执行

    dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

    参数说明:queue--队列,block--任务

     

    2、两种队列(串行队列、并发队列)

    1)串行队列

    让任务一个接一个地执行,也就是说,只有当前一个任务执行完毕后,再开始执行下一个任务。

    2)并发队列

    可以让多个任务并发(同时)执行。

    注意: 不管是串行队列还是并发队列,队列中任务的取出都遵循队列的FIFO原则,即先进先出,后进后出原则。

    将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行。

     

    -----未完待续2021年6月10日

     

     

     

     

     

     

     

  • 相关阅读:
    今天解决了一个很郁闷的问题!
    解决了安装golive后html文件图标显示错误的问题
    [转载]Asp.Net 2.0 发布问题
    使用 Visual Studio 2005 构建“WPFE”项目
    Ajax学习网址备忘录
    [原创首发]深圳博客问测系统正式发布啦!
    如何在用户控件里联动Dropdownlist
    [转载]在ASP.NET中值得注意的两个地方
    [转]Prototype 1.5 Ajax 使用教程
    1038 Recover the Smallest Number (30 分)(贪心)
  • 原文地址:https://www.cnblogs.com/cchHers/p/14835445.html
Copyright © 2011-2022 走看看