iOS多线程调研

稀土 2017-12-19

关于多线程的基础释义就不多做解释了,下面引用一句百度百科上的话作为开头:

多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

本文涉及内容

  • 多线程的目的
  • 多线程的优缺点
  • 为什么要用多线程
  • 什么时候使用多线程
  • iOS线程状态
  • iOS线程安全
  • iOS常见多线程
    • Pthread
    • NSThread
    • NSOperation
    • Grand Central Dispatch(GCD)

一. 目的

随着计算机技术的发展,编程模型也越来越复杂多样化。但多线程编程模型是目前计算机系统架构的最终模型。随着CPU主频的不断攀升,X86架构的硬件已经成为瓶,在这种架构的CPU主频最高为4G。事实上目前3.6G主频的CPU已经接近了顶峰。如果不能从根本上更新当前CPU的架构(在很长一段时间内还不太可能),那么继续提高CPU性能的方法就是超线程CPU模式。那么,作业系统、应用程序要发挥CPU的最大性能,就是要改变到以多线程编程模型为主的并行处理系统和并发式应用程序。所以,掌握多线程编程模型,不仅是目前提高应用性能的手段,更是下一代编程模型的核心思想。多线程编程的目的,就是"最大限度地利用CPU资源",当某一线程的处理不需要占用CPU而只资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源。从根本上说,这就是多线程编程的最终目的。也就是说多线程的目的不是为了提高运行效率,而是为了提高资源使用效率,为了最大限度地利用CPU资源。其实仔细想想,如果这就是正确答案的话就不可避免的产生一个矛盾。假设一下,有一个任务需要分成不等的小任务执行,如果按照上述结果看,最好是分成尽可能多的线程并发处理,这样可以最大限度的利用CPU的资源,并且可以最短时间内完成任务。但是某些情况会出现不一样的情况,比如说这个任务是数据库的增删改查,为了保证数据的正确性我们需要进行加锁同步等处理。任务执行时CPU还需要不断的切换子线程进行任务处理,这样看不光没有提高运行效率,反而拉低了运行效率,而且大量占用了CPU资源,浪费很多的内存空间(默认情况下,主线程占用1M,子线程占用512KB),那么到底该不该使用多线程?

二. 优缺点

从上述问题来看,到底该不该使用多线程,为什么要用多线程就是个问题了。为了想清楚这个问题,接下来先了解一下多线程的优缺点:

1. 多线程的优点

  • 使用线程可以把占据时间长的程序中的任务放到后台去处理
  • 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
  • 程序的运行速度可能加快
  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。
  • 多线程技术在IOS软件开发中也有举足轻重的位置。
  • 线程应用的好处还有很多,就不一一说明了-- 引自百度百科

2. 多线程的缺点

  • 如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。
  • 更多的线程需要更多的内存空间。
  • 线程可能会给程序带来更多“bug”,因此要小心使用。
  • 线程的中止需要考虑其对程序运行的影响。
  • 通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。-- 引自百度百科

三. 为什么要用多线程

为什么要使用多线程,维基百科上有这样一段话:

一个线程持续运行,直到该线程被一个事件挡住而制造出长时间的延迟(可能是内存load/store操作,或者程序分支操作)。该延迟通常是因缓存失败而从核心外的内存读写,而这动作会使用到几百个CPU周期才能将数据回传。与其要等待延迟的时间,线程化处理器会切换运行到另一个已就绪的线程。只要当之前线程中的数据送达后,上一个线程就会变成已就绪的线程。这种方法来自各个线程的指令交替执行,可以有效的掩盖内存访问时延,填补流水线空洞。举例来说:周期 i :接收线程 A 的指令 j周期 i+1:接收线程 A 的指令 j+1周期 i+2:接收线程 A 的指令 j+2,而这指令缓存失败周期 i+3:线程调度器介入,切换到线程 B周期 i+4:接收线程 B 的指令 k周期 i+5:接收线程 B 的指令 k+1在概念上,它与即时操作系统中使用的合作式多任务类似,在该任务需要为一个事件等待一段时间的时候会主动放弃运行时段。-- 维基百科

假设一下,如果你的程序是单线程,然后网络比较慢的情况下下载了一张图片,我的天,用户可以洗洗睡了。再假设一下,现在有三个任务需要处理。单独一条线程处理它们分别需要5、3、3秒。如果三个CPU并行处理,那么一共只需要5秒。相比于串行处理,节约了6秒。而同步/异步,描述的是任务之间先后顺序问题。假设需要5秒的那个是保存数据的任务,而另外两个是UI相关的任务。那么通过异步执行第一个任务,我们省去了5秒钟的卡顿时间。对于同步执行的三个任务来说,系统倾向于在同一个线程里执行它们。因为即使开了三个线程,也得等他们分别在各自的线程中完成。并不能减少总的处理时间,反而徒增了线程切换(这就是文章开头举的例子)对于异步执行的三个任务来说,系统倾向于在三个新的线程里执行他们。因为这样可以最大程度的利用CPU性能,提升程序运行效率。综合上述而言我们可以得出结论,在需要同时处理IO和UI的情况下,真正起作用的是异步,而不是多线程。可以不用多线程(因为处理UI非常快),但不能不用异步(否则的话至少要等IO结束)。也就是说异步方法并不一定永远在新线程里面执行,反之亦然。如果这样说,那什么时候多线程才会起到真正意义上的效率?什么时候该使用多线程进行程序开发?

四. 什么时候使用多线程

首先来了解一下这个概念:“多线程的使用主要是用来处理程序‘在一部分上会阻塞’,‘在另一部分上需要持续运行’的场合”。一般是根据需求,可以用多线程,事件触发,callback等方法达到。但是有一些方法是只有多线程能办到的就只有用多线程或者多进程来完成。举个简单的例子,能理解就行。假设有这样一个程序,1会不停的处理收到的所有TCP请求。对于每个TCP请求做不同的操作。不能有遗漏2有很多特定的请求会向一个服务器发送存储的数据,或者是等待用户输入。来看看。第1个要求很简单。用个while循环就搞定了。但第2个特性呢。一旦在等待用户输入或者是连接服务器时,程序会“阻塞”一段时间,这一段时间内就无法处理其他的TCP请求了。所以可以利用多线程,每个线程处理不同的TCP请求。这样程序就不会“阻塞”掉了。总之,凡事自然有利有弊,多线程带来了高效率的同时也带来了一定程度我不稳定性,上述内容只是在多线程本身的基础上得出的结论,如果综合线程安全,同步,通信等情况去看就会变得更加负责。总结一下就是,当应用在前台操作的同时还需要进行后台的计算或逻辑判断的情况可以使用多线程,但是需要综合考虑使用多线程的不稳定性,尽量避免由于使用多线程而产生的新问题。

五. iOS线程状态

一般来说,线程有五个状态

  • 新建状态:线程通过各种方式在被创建之初,还没有调用开始执行方法,这个时候的线程就是新建状态;
  • 就绪状态:在新建线程被创建之后调用了开始执行方法,但是CPU并不是真正的同时执行多个任务,所以要等待CPU调用,这个时候线程处于就绪状态。处于就绪状态的线程并不一定立即执行线程里的代码,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。
  • 运行状态:线程获得CPU时间,被CPU调用之后就进入运行状态
  • CPU 负责调度可调度线程池中线程的执行
  • 线程执行完成之前(死亡之前),状态可能会在就绪和运行之间来回切换
  • 就绪和运行之间的状态变化由 CPU 负责,程序员不能干预
  • 阻塞状态:所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
  • 线程通过调用sleep方法进入睡眠状态
  • 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者
  • 线程试图得到一个锁,而该锁正被其他线程持有;
  • 线程在等待某个触发条件
  • 死亡状态:当线程的任务结束,发生异常,或者是强制退出这三种情况会导致线程的死亡。线程死亡后,线程对象从内存中移除。一旦线程进入死亡状态,再次尝试重新开启线程,则程序会挂。

七. iOS线程安全

一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件和同一个方法等。因此当多个线程访问同一块资源时,很容易会发生数据错误及数据不安全等问题。因此要避免这些问题,我们需要使用某些方式保证线程的安全,比如“线程锁”。

这边有一段代码,可以先用来测试一下:

@property (atomic, assign) int intA;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //thread A
        for (int i = 0; i < 10000; i ++) {
            self.intA = self.intA + 1;
            NSLog(@"Thread A: %d\n", self.intA);
        }
    });

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //thread B
        for (int i = 0; i < 10000; i ++) {
            self.intA = self.intA + 1;
            NSLog(@"Thread B: %d\n", self.intA);
        }
    });

即使将intA声明为atomic,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1;不是原子操作,虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作,当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值。这种场景就是多线程不安全的表现之一。

为了更加安全的使用多线程,也为了代码可以正确的执行,我们需要一种保证线程安全的机制,“线程锁”就诞生了,接下来将简单的了解一下iOS中的常用锁。在网上查找了有些资料,发现大多数资料只介绍了以下几种锁:

  1. OSSpinLock (暂不建议使用,原因参见这里)
  2. dispatch_semaphore
  3. pthread_mutex
  4. NSLock
  5. NSCondition
  6. pthread_mutex(recursive)
  7. NSRecursiveLock
  8. NSCondition
  9. @synchronized

这张图片是在网上找到的关于这七种锁的性能测试:

iOS多线程调研当然除了这七种锁之外iOS还提供了很多其他的锁,比如C语言实现的读写锁(Read-write Lock),自旋锁(Spin Lock)等这里就不做对锁的解释了,文章下边会有链接对各种锁在iOS中的应用做详细解释,还有配合iOS常用多线程方法写的一些小demo,对这方面有兴趣的可以去看一下。

六. iOS常见多线程使用方法

接下来就介绍一下iOS常见的几种多线程实现方式,因为篇幅比较长,所以写到另外几篇文章里边:

1. Pthreads

这篇文章主要是介绍Pthreads的,里边会有Pthreads的基础释义,也有常用API与属性的介绍,当然也会介绍一些常用锁和Pthreads的配合使用,链接在这Pthread。然而里边并不会针对全部的锁做解析,只是针对某几种锁做释义解析以及与Pthreads的配合使用。

2. NSThread

然后是NSThread这个,在网上找了很多资料,看了很多文章,但是总是不太符合自己的心意。刚好公司有机会让我参与多线程的调研,就试着总结了一下它的常用属性与API,也有一些加锁代码。

3. GCD

GCD的内容太多了,到发出此链接的时候已经修改过三次了。还是有很多知识没有涉及到,希望以后有时间补上吧。Grand Central Dispatch,这是GCD调研结果的链接。####4. NSOperation全程看着官方文档写的,附带了一些应用实例,函数解析等NSOperation。

以上,就是本人对多线程的调研结果。调研期间我看到这样一句话“我们的目的不是研究出多么牛逼的锁,而是研究安全更高效的多线程方式”,当然,在没有没这样牛逼的多线程实现方式的时候,还是有必要了解一下各种锁的优缺点的。

有志者、事竟成,破釜沉舟,百二秦关终属楚;苦心人、天不负,卧薪尝胆,三千越甲可吞吴.

相关推荐