三十三、多线程

664 阅读11分钟

本文由快学吧个人写作,以任何形式转载请表明原文出处。

一、资料准备

《POSIX多线程程序设计》,支持正版。所以自己找资源吧。

苹果官方关于线程的文档 : 苹果官方线程文档

二、线程和进程的概念

1. 什么是进程

(1). 进程的理解

进程可以分成广义和狭义的解释 :

广义 :

进程是 : 一个具有一定独立功能的程序,关于某个数据集合的活动。

进程是操作系统动态执行的基本单元,既是基本分配单元,也是基本执行单元。也就是说,操作系统想要动态的运行起来,必须得有进程。没有进程,操作系统就不能执行。

狭义 :

进程就是一个正在运行的程序实例。

以iOS的app举例,原则上iOS是不支持多进程的。这个不支持的意思就是 : 每个进程之间的运行都是独立的,每个进程都在它自己专用的、收到保护的内存空间上执行,拥有着独立运行所需要的全部资源。

仅以iOS为例,可以更狭义的理解进程 :

仅以iOS原则为例,更狭义的说,一个进程就是一个正在运行的app。

(2). iOS为什么是单进程

(1). iOS系统是沙盒模式的设计,一个进程一个sandBox。进程与进程之间是不可以互相访问和利用资源的。这使得每个进程的资源都更加的安全、隐私性更加的优秀。

(2). 进程之间的切换和通信,可以使效率更高,但是随之而来的是资源消耗的增大。

(3). 个人猜测 : 没必要多进程。因为苹果的官方工程师都是大牛,没必要给我这种小菜这么大的权限可以切换进程。因为多进程就意味着更多的资源,做不好资源的优化,只会使app越来越卡。

2. 什么是线程

在计算机中,线程是一种能够实现某种功能的基本单元。而针对iOS来说,因为iOS原则上不支持多进程,我们可以更具体的理解这句话为 :

线程是进程的基本单元。是程序执行流的最小单元。是处理器调度的基本单位。一个进程的的所有任务都在线程中执行。

针对iOS :

  1. 针对iOS,进程想要执行任务,必须至少拥有一条线程。
  2. 在iOS进程(就是app)启动的时候,默认会开启一条线程,这条默认线程就是我们常说的主线程。也可以叫它UI线程,因为这条默认的线程负责的是处理UI事件,包括显示和刷新。

3. 进程和线程之间的关联

  1. 地址空间共享。所谓地址空间共享,就是同一个进程中的所有线程,共享这个进程的地址空间。
  2. 资源分配共享。同一个进程中的所有线程,共享这个进程的资源,包括但不限于 : 内存、I/O、CPU。
  3. iOS中,线程才是处理器调度的基本单位。

4. 多线程

4.1 多线程存在的意义

多线程是为了使进程中的任务处理不延误,一个任务的延误不耽误其他任务的执行。加快了任务的处理效率。

4.2 单核CPU实现多线程的原理

多核才存在真正意义上的多线程,单核的多线程只是一个伪概念。

正常来说,单核的CPU,在同一个时间点,只能处理一个任务。但是如果单核CPU在单位时间里,在多条线程之间快速切换调度,就会造成一种单核CPU可以多线程并行的假象。这种行为或者说思想,也被叫做时间片轮转。

为什么要不停的时间片轮转呢?

举个例子说,有5个人,饿的不行了,突然遇到了一个人给他们喂饭吃,如果让第一个人吃饱了,再让第二个人吃,然后再让第三个人吃,直到第五个人,那么第五个人可能还没等到前四个人吃完呢,就gg了。为了不让后面吃饭的人gg,最好的办法就是一人吃一口,都先别奔着吃饱使劲,都奔着别gg使劲 。

这5个人就是线程,这个喂饭的人就是单核CPU。这么做既是为了让所有的线程都可以存活,也自然的提升了单核CPU的利用率,也是利益最大化的体现。

4.3 多线程的优缺点

优点 :

  1. 可以适当的提高程序的执行效率。
  2. 可以适当的提高资源的利用率。例如 : CPU和内存的利用率。
  3. 线程上的任务执行完成之后,线程是可以自动销毁的。

缺点 :

  1. 对比多进程来说,多线程是不够健壮的。

例如 : 如果是多进程,当一个进程crash之后,是不会影响其他的进程。但是多线程就不行,同一个进程中的多线程,一旦有一个线程发生问题,比如crash,那么整个程序都会crash。

  1. 多线程是要占用内存空间的。

例如 : iOS中进程默认开启的主线程是需要1M的栈区内存。其他的二级线程要占用512K的栈区空间。

  1. 多线程影响CPU性能

例如 : CPU在多条内存之间快速切换调用是需要性能的,要调用的线程越多,CPU切换的就越频繁,这就会导致每条线程被调用的频次就会降低,频次越低,线程里的任务执行的效果就越差,如果想要保证每条线程里的任务都可以完成的很好,就要加大资源开销,让人在感官上感受不到执行效率变差了。

  1. 多线程的程序的设计就会更复杂

例如 : 多线程之间如何通信、多线程的数据共享。

5. 线程的生命周期

  1. 创建。可以理解为创建一个线程对象。
  2. 就绪。线程是可以运行的状态,并且被加入了可调度线程池,但是在等待CPU的调度。比如进程刚刚启动,或者这个进程刚从阻塞状态中恢复,或者可能被调度的权利被其他的线程抢占了。
  3. 运行。线程正在执行自己内部的任务,也就是被CPU调度了。在线程执行完任务之前,线程的状态可能会在就绪和运行之间多次切换,这个切换,是由CPU决定的,不受我们的控制。
  4. 阻塞。线程处于无法执行任务的状态,比如调用了sleep()、等待同步锁(@synchronized),从可调度线程池中移除。
  5. 销毁。线程中的任务执行完毕了。执行完毕就可以退出。也可能是满足某个条件之后,在线程的内部或者主线程中手动终止线程的执行,进而结束执行任务,进行销毁。
  6. 线程绝大多数的时间都处于2,3,4这三种状态,也就是 : 就绪、运行、阻塞。

对于线程的销毁,有两种常见的方法 : cancelexit,它们的区别是 :

exit : 强制终止线程。只要exit,后面的所有代码都不会执行。

cancel : 不会强制终止正在执行的线程。终止的只是当前的线程。

图片.png

6. 线程池

容纳线程的容器,就叫线程池。线程池存在三种容量 : 最大容量,核心容量,当前容量。

线程池是如何容纳线程的 :

图片.png

四个缓存策略 :

AbortPolicy : 饱和策略的默认策略。当前的线程池已经与核心线程池的容量一致,并且当前线程池的工作队列已经满了,并且当前线程池中的所有线程都在工作。那么就只能在新任务要加入的时候抛出异常(调用这个接口RejectedExecutionExeception),这个异常可以由调用者捕获。

CallerRunsPolicy : 这是饱和策略的调节策略。既不放弃任务,也不抛出异常,而是将某些任务回退到调用者。它不会在线程池的线程中执行新的任务,而是在exector线程中执行新的任务。

DiscardPolicy : 直接抛弃新提交的任务。

DiscardOldestPolicy : 抛弃最长时间没执行的任务,就是队列头部的任务,然后尝试提交新的任务。这种策略不适合工作队列为优先队列的场景。

三、iOS中的多线程

1. 多线程方案

常见的4种iOS的

方案简介语言线程生命周期使用频率
pthread1. 一套通用的多线程API;
2. 适用于UnixLinuxWindows等系统;
3. 具有跨平台性、可移植性;
4. 使用难度较大。
C程序员管理极少使用(不是大牛,不建议直接操作pthread)
NSThread1. 面向对象
2. 比pthread简单,直接操作线程对象
OC程序员管理较少使用
GCD1. 苹果推荐,替代NSThread
2. 充分利用设备的多核
C自动管理经常使用
NSOperation1. 基于GCD实现
2. 比GCD多一些简单实用的功能
3. 更加的面向对象
OC自动管理经常使用

2. 举例

- (void)jd_thread_test:(NSString *)name
{
    NSLog(@"\n方式 : %@\n当前线程 : %@ -- 主线程 : %@",name,[NSThread currentThread],[NSThread mainThread]);
}

void *jd_pthread_use(void *param)
{
    NSLog(@"\n方式 : %@\n当前线程 : %@ -- 主线程 : %@",param,[NSThread currentThread],[NSThread mainThread]);
    return NULL;
}

- (IBAction)jd_pthread:(UIButton *)sender {
    
    // 定义一个线程标识符,程序中用线程标识符来表示线程
    pthread_t jd_thread;
    /**
     参数 :
     1. pthread_t类型的线程变量,要传地址,如果不是指针,那就&取地址
     2. 线程的属性,不知道填什么,或者没什么特定的情况,就填NULL
     3. 线程中要执行的函数的地址,也就是说,不要在这里传参数,直接把函数名放进去就行了
     4. 3中的函数的参数
     */
    pthread_create(&jd_thread,NULL,jd_pthread_use,@"pthread");
}

- (IBAction)jd_NSThread:(UIButton *)sender {
    [NSThread detachNewThreadSelector:@selector(jd_thread_test:) toTarget:self withObject:@"NSThread"];
}

- (IBAction)jd_gcd:(UIButton *)sender {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self jd_thread_test:@"GCD"];
    });
}

- (IBAction)jd_NSOperation:(UIButton *)sender {
    NSOperationQueue *q = [[NSOperationQueue alloc] init];
    [q addOperationWithBlock:^{
        [self jd_thread_test:@"NSOperation"];
    }];
}

3. 线程间的通讯

苹果官方给了7种通信机制 : 官方线程通信机制

翻译一下 :

通信机制描述
直接消息传递Cocoa应用程序支持直接在其他线程上执行方法。
这意味着一个线程可以直接在其他任何的线程上执行一个方法。
因为方法是在目标线程的上下文执行的,所以以这种方式通信发送的消息会自动在该线程上自动化。
全局变量、
共享内存、
对象
在两个线程间通信,另一种简单的方法就是通过全局变量、共享内存块、对象。
这种方法快速而简单,但是对比直接消息传递更脆弱。必须使用锁或者其他同步机制保护共享的变量,确保代码的正确。
如果不这样做的话,可能会引发竞态条件、损坏数据甚至崩溃。
ConditionsConditions是一种同步工具,本身是一种特殊类型的锁。
使用Conditions可以控制线程中特定代码的执行时间。
可以把Conditions看作一个守卫,只有条件满足了,才允许线程运行。
Run loop sources通过自定义Runloop source的配置,可以用来让线程接收特定消息。
由于Runloop source是依靠事件来驱动的,所以Runloop source在无事可做的时候,会让线程进入自动休眠状态,这也提高了线程的效率。
Ports and sockets基于端口的通信是两个线程之间更复杂的一种通信方法,但是它更可靠。
端口和套接字还可以用于与其他进程和服务通信。
为了提高效率,端口是通过Runloop source实现的,所以端口上没有数据的时候,也会让线程进入休眠状态。
消息队列传统的多进程服务定义了FIFO抽象队列,用来管理消息的传入和传出。
消息队列优点是简单方便。
缺点是没有其他通信机制的高效率。
Cocoa分布式对象基于Cocoa的分布式对象,它提供了基于端口通信的高级实现,可以作用于线程间通信。但是资源开销大。
可以尝试用它做进程间通信,而不是线程间通信。