第二十五节—线程

2020-11-14  本文已影响0人  L_Ares

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

本节介绍线程的一些基本知识,为了后续开启和iOS相关的线程内容。个人建议要深入学习线程的话,可以看一下《posix多线程程序设计》。本章节只是介绍我认为需要介绍的基本知识,包括后续章节主要都是和iOS开发相关的线程探索。

下面有一份苹果官方的线程文档,毕竟苹果也是要面子的人,线程这么重要的东西,肯定会有自己的官方文档的,你会发现,苹果的线程实现和pthread息息相关,但是个人不建议直接操控pthread,因为操控底层一般都是很"危险"的事情。

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

一、线程基础

1. 什么是进程

进程的定义分为广义和狭义两种。

  • 广义 : 进程是一个具有一定独立功能的程序关于某个数据集合的一次活动,是操作系统动态执行的基本单元,即是基本分配单元,又是基本执行单元。
  • 狭义 :进程是一个正在运行的程序实例。

说直白一点,仅以iOSApp为例,因为iOS是不支持多进程的,每个进程之间都是相互独立的,每个进程均在其专用的、受保护的内存空间上运行,拥有独立运行所需要的全部资源,所以你可以直接用进程的狭义定义理解,也就是 :

仅针对iOS来说,进程可以理解为一个正在运行App

2. 什么是线程

在计算机中,线程是一种能够实现某种功能的基本软件单元。而在iOS中,因为没有多进程App,我们可以具体化这句话 :

线程是进程的基本执行单元。是程序执行流的最小单元,一个进程的所有任务都在线程中执行。

3. iOS中的线程

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

4. 线程和进程的关联与区别

  • 地址空间 : 同一个进程的所有线程共享本进程的地址空间
  • 资源分配 : 同一个进程内的所有线程共享本进程的资源,例如:内存、I/OCPU等。
  • 基本单位 : 在iOS中,线程才是处理器调度的基本单位

5. 多线程

5.1 为什么要多线程?

为了在执行任务的时候不延误其他任务的执行。加快任务的处理效率。

5.2 多线程的原理?

对于单核CPU来说,同一时间,CPU只能处理一个线程的任务,所以多线程的同时执行,其实只是单核CPU在单位时间里,多条线程之间快速的切换调度,造成了单核CPU多线程并行的假象。也有人叫它时间片轮转。

6. 多线程优缺点

这里就要知道在iOS中虽然多进程是不行的,但是多线程是非常普遍的。所以说优缺点的话,我们主要说的是多线程的优缺点。

优点 :
  • 可以适当的提高程序的执行效率。
  • 可以适当的提高资源的利用率,比如CPU内存利用率
  • 线程上的任务执行完成后,线程是可以自动销毁的。
缺点 :
  • 线程不够"健壮"。
    例如 : 对比多进程,进程crash后,会有保护模式,不会影响其他的进程。但是多线程如果crash了一个线程,整个进程都会崩溃。
  • 开线程会占用一定的内存空间。
    例如 : 一般情况下,iOS系统中,主线程占有1M的栈区内存,其他的二级线程占有512K的栈区内存。
  • 线程越多,CPU在调用线程的时候的性能开销越大。
    原因 : CPU在线程间切换是需要资源的,要调用的线程越多,切换越频繁,每条线程被调用的频次越低,线程里面的任务执行的就越差。想要保证执行效率,就要加大资源的开销,让人感官上感受不到执行效率的变差。
  • 程序的设计更复杂。
    原因 : 多线程间的通信,多线程的数据共享。

7. 线程的生命周期

(1). 新建 : 首先,你要有一个线程,新建可以理解为创建一个线程的实例对象。

(2). 就绪 : 线程是能够运行的状态,并且被加入了可调度线程池,但是在等待CPU调度。可能因为刚刚启动、刚从阻塞中恢复、或者可能被其他线程抢占。

(3). 运行 : 线程正在执行任务,也就是被CPU调度了,在线程执行完任务之前,线程的状态可能在就绪运行间发生多次切换,这个切换由CPU决定,不由我们管理。

(4). 阻塞 : 处于一种无法执行任务的状态。比如调用了sleep、等待同步锁(@synchronized)、从可调度线程池移出。

(5). 销毁 : 任务执行完毕,就可以退出了。也可以是满足某个条件后,在线程的内部或者主线程中手动终止线程的执行,从而退出线程。

线程绝大多数时间都处于它生命周期中的三个状态 : 就绪运行阻塞

另外,在销毁的时候,有cancelexit两种常见的方法,它们也是有区别的 :

  • exit : 强行终止线程。后续的所有代码都不会执行。
  • cancel : 不可以强行终止正在执行的线程。终止的也只是当前的线程。

来张图,看的清楚点,看看线程在其生命周期中都经历了什么。

图1.7.0.png

8. 线程池

这个就顾名思义了,线程池,就是拿来装线程的,一般情况下,以这种作为概念的设计,都会存在三种容量大小 : 最大容量核心容量当前容量

下面也是直接上图吧,文字表述容易乱掉,这个图就可以理解线程池的一个原理。

图1.8.0.png

四个缓存策略 :

  1. Abort策略 : 这是饱和策略的默认策略。当线程池的大小已经和核心线程池大小一样大,并且工作队列已经饱和,工作队列中的线程也都在工作,那么当新的任务提交到线程的时候,直接抛出未检查异常,也就是RejectedExecutionExeception,该异常可由调用者捕获。
  2. CallerRuns策略 : 这是饱和策略的调节策略,即不放弃任务也不抛出异常,而是将某些任务回退到调用者。
    它不会在线程池的线程中执行新的任务,而是在exector的线程中运行新的任务。
  3. Discard策略 : 这是饱和策略中很直接的策略,直接抛弃新提交的任务。
  4. DiscardOldest策略 : 抛弃最长时间都没执行的任务,也就是队列头上的任务,然后尝试提交新任务。(不适合工作队列为优先队列的场景)。

二、iOS中的多线程

iOS开发中,多线程有4种大家常见的实现方案,其中以GCD最受官方推荐,以pthread使用最少,原因很简单,pthread是更底层的多线程实现方案,虽然可以有更多的自主性,但是容易引发问题。

1. 多线程方案

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

2. 多线程方案举例

pthread#import <pthread.h>

#pragma mark - pthread
- (void)jd_pthread
{
    //定义一个线程标识符,程序中使用线程标识符来表示线程
    pthread_t thread;
    
    /**
     创建线程 :
     int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
                        const pthread_attr_t * _Nullable __restrict,
                        void * _Nullable (* _Nonnull)(void * _Nullable),
                        void * _Nullable __restrict);
     参数 :
       1. pthread_t *restrict            : 线程变量的指针
       2. const pthread_attr_t *restrict : 线程的属性
       3. void* (*)(void*)               : 线程中要执行的函数的起始地址
       4. void *restrict                 : 线程中要执行的函数的参数
     */
    pthread_create(&thread, NULL, jd_pthread_test, NULL);
    
    pthread_detach(thread);
    
}

void *jd_pthread_test(void *param)
{
    
    NSLog(@"jd_pthread_test : %@ --- %@", [NSThread currentThread],[NSThread mainThread]);
    
    return NULL;
}

#pragma mark - NSThread
- (void)jd_nsthread
{
    [NSThread detachNewThreadSelector:@selector(thread_use_method:) toTarget:self withObject:@"jd_nsthread"];
}

#pragma mark - GCD
- (void)jd_gcd
{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self thread_use_method:@"jd_gcd"];
    });
}

#pragma mark - NSOperation
- (void)jd_nsOperation
{
    NSOperationQueue *opqueue = [[NSOperationQueue alloc] init];
    [opqueue addOperationWithBlock:^{
        [self thread_use_method:@"jd_nsOperation"];
    }];
}

#pragma mark - 线程调用的方法
- (void)thread_use_method:(NSString *)name
{
    NSLog(@"%@ : %@ --- %@", name, [NSThread currentThread],[NSThread mainThread]);
}

三、线程间的通信

iOSThreading Programming Guide官方文档里面提及了Communication mechanisms也就是通信机制,一共展示了7种线程间通信机制 :

图1.0.0.png

下面一一的翻译介绍一下 :

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

注: 一个小问题

Q : 苹果,应该说iOS为什么不像安卓一样可以多进程,使用多进程通讯?

A :
(1). 因为进程之间的切换,消耗的资源非常的大,虽然效率是蛮高的。

(2). 另外在设计方面,苹果的沙盒使得资源更加的安全,隐私性更加的好,别的App很难拿到另外的App的内容,这也是为什么iOS相比安卓更加流畅的原因之一。

(3). 这是个人猜想,苹果应该是认为iOS已经做到了非常大的优化和高效率了,没有必要给开发人员那么大的权限去切换进程玩,因为多进程意味着你对别的应用程序的骚扰性和影响都很大。

上一篇下一篇

猜你喜欢

热点阅读