iOS-25.多线程原理

537 阅读12分钟

ios底层文章汇总

线程和进程

线程

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

进程

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
  • 通过“活动监视器”可以查看mac系统中所开启的线程

进程是线程的容器,而线程用来执行任务。在iOS中是单进程开发,一个进程就是一个app进程之间是相互独立的,如支付宝、微信、qq等,这些都是属于不同的进程

进程与线程的关系

  • 线程是依附于进程的,不能独立存在,它包含在进程之中,是进程中的实际运作单位。进程一旦结束,所有线程都结束。
  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

进程与线程之间的关系主要涉及两个方面

线程是进程中的一个执行单元,由CPU独立调度执行,负责当前进程中任务的执行。一个进程可以有一个或多个线程,同一进程中的多个线程将共享程序的内存空间,也就是该进程中的代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)等系统资源。

 进程和线程的区别

  • 根本区别

    进程是操作系统分配资源的最小单位,线程程序执行的最小单位

  • 地址空间

    • 同一个进程的线程共享本进程的地址空间
    • 进程之间则是独立的地址空间
  • 资源拥有

    • 同一个进程内线程共享本进程的资源,如内存、I/O、cpu等
    • 但是进程之间资源是独立的

两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即 进程相当于一个工厂线程相当于工厂中的一条流水线

  • 多进程要比多线程健壮

    • 一个进程崩溃后,在保护模式下不会对其他进程产生影响
    • 一个线程崩溃整个进程都死掉
  • 调度和切换

    • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程
    • 同样如果要求同时进行并且又要共享某些变量的并发操作只能用线程不能用进程
  • 执行过程

    • 每个独立的进程有一个程序运行的入口、顺序执行序列程序入口
    • 但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

线程和Runloop的关系

  • runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局 的字典里。
  • runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。
  • runloop在第一次获取时被创建,在线程结束时被销毁。
  • 对于主线程来说,runloop在程序一启动就默认创建好了。
  • 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意确保子线程的runloop被创建,不然定时器不会回调

多进程 vs 多线程

多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。混合多进程和多线程的程序涉及到同步、数据共享的问题,这种模型更复杂,实际很少采用。

和多进程相比,多线程的优势在于:

  • 线程的调度与切换比进程很多,同时创建一个线程的开销也比进程要小很多;
  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,线程间通信就是读写同一个变量,速度很快。而进程之间的通信需要以通信的方式(Inter Process Communication,IPC)进行。

多进程的优点在于:

  • 多进程程序更健壮,在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

多线程

单核CPU是怎么执行多任务的呢?

时间片轮转调度:简单地说就是把一个处理器划分为若干个短的时间片,每个进程会被操作系统分配一个时间片(即每次被 CPU 选中来执行当前进程所用的时间),每个时间片依次轮流地执行处理各个应用程序,时间一到,无论进程是否运行结束,操作系统都会强制将 CPU 这个资源转到另一个进程去执行,由于一个时间片很短,从而达到多个应用程序在同时进行的效果。

多线程原理

  • 对于单核CPU同一时间,CPU只能处理一条线程,即只有一条线程在工作,
  • iOS中的多线程同时执行的本质是 CPU在多个任务之间进行快速的切换,由于CPU调度线程时间足够快,就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片

多线程意义

  • 能适当提高程序的执行效率

  • 能适当提高资源的利用率,如CPU、内存

  • 线程上的任务执行完成后,线程会自动销毁

  • 开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB

  • 如果开启大量线程,会占用大量的内存空间,降低程序的性能

  • 线程越多,CPU在调用线程上的开销就越大

  • 程序设计更加复杂,比如线程间的通信,多线程的数据共享

多线程生命周期

多线程的生命周期主要分为5部分:新建 - 就绪 - 运行 - 阻塞 - 死亡,如下图所示

image.png

多线程声明周期

  • 新建(new) 主要是实例化线程Thread对象,创建完成后就需要为线程分配内存。当线程处于"新线程"状态时,仅仅是一个空线程对象,它还没有分配到系统资源。因此只能启动start()或终止stop()它。任何其他操作都会引发异常。

  • 就绪(Runnable) 线程对象调用start()方法,将线程对象加入可调度线程池等待CPU的调用,即调用start方法,并不会立即执行,进入就绪状态,需要等待一段时间,经CPU调度后才执行run(),也就是从就绪状态进入运行状态

  • 运行(Running) 就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程

  • 阻塞(Blocked)

    当满足某个预定条件时,可以使用休眠,即sleepsuspend(),IO阻塞wait()等待通知,等待同步锁,阻塞线程执行。

    下面关于休眠的时间设置,都是NSThread

    • sleepUntilDate: 阻塞当前线程,直到指定的时间为止,即休眠到指定时间
    • sleepForTimeInterval: 在给定的时间间隔内休眠线程,即指定休眠时长
    • 同步锁:@synchronized(self):

stop()时间到resume(),IO阻塞返回收到通知获得同步锁等,线程从阻塞状态进入就绪状态,等待CPU调度。

  • 死亡(Dead)

    • 正常死亡,即线程执行完毕
    • 非正常死亡,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit,stop方法等退出),或者发生了错误Error或异常Exception

总结

简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片),

  • 如果时间片用尽,线程就会进入就绪状态队列
  • 如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列
  • 等待事件发生后,线程又会重新进入就绪状态队列
  • 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列选择一个线程继续执行

线程的exitcancel说明

  • exit:一旦强行终止线程,后续的所有代码都不会执行
  • cancel:取消当前线程,但是不能取消正在执行的线程

线程的优先级越高,是不是任务的执行越快?

并不是,线程执行的快慢,除了要看优先级,还需要查看资源的大小(即任务的复杂度)、以及 CPU 调度情况。在NSThread中,线程优先级threadPriority已经被服务质量qualityOfService取代,以下服务质量枚举

image.png

线程池原理

image.png 线程池原理

  • 【第一步】判断核心线程池是否都正在执行任务

    • 返回NO,创建新的工作线程去执行
    • 返回YES,进入【第二步】
  • 【第二步】判断线程池工作队列是否已经饱满

    • 返回NO,将任务存储到工作队列,等待CPU调度
    • 返回YES,进入【第三步】
  • 【第三步】判断线程池中的线程是否都处于执行状态

    • 返回NO,安排可调度线程池中空闲的线程去执行任务
    • 返回YES,进入【第四步】
  • 【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)

    • AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行
    • CallerRunsPolicy:将任务回退到调用者
    • DisOldestPolicy:丢掉等待最久的任务
    • DisCardPolicy:直接丢弃任务

image.png 1.如果当前运行的线程少于corePoolSize,则创建新线程来执行任务

2.如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。 线程池会让corePoolSize里执行完任务的线程,反复的获取BlockingQueue的任务执行。

3.如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务

4.如果创建新线程将使当前运行的线程超出maximumPoolSize,就启用饱和策略。

iOS中多线程的实现方案

iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation,汇总如图所示

image.png

//pthread
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
/**
 pthread_create 创建线程
 参数:
 1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
 同时不需要 `*`
 2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
 3. 线程要执行的`函数地址`
 void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
 (*): 函数名
 (void *): 参数类型,void *
 4. 传递给第三个参数(函数)的`参数`
 */
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
    NSLog(@"成功");
} else {
    NSLog(@"失败");
}
    
//NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
    
//GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self threadTest];
});
    
//NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
    [self threadTest];
}];

- (void)threadTest{
    NSLog(@"begin");
    NSInteger count = 1000 * 100;
    for (NSInteger i = 0; i < count; i++) {
        // 栈区
        NSInteger num = i;
        // 常量区
        NSString *name = @"zhang";
        // 堆区
        NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
        NSLog(@"%@", myName);
    }
    NSLog(@"over");
}

void *pthreadTest(void *para){
    // 接 C 语言的字符串
    //    NSLog(@"===> %@ %s", [NSThread currentThread], para);
    // __bridge 将 C 语言的类型桥接到 OC 的类型
    NSString *name = (__bridge NSString *)(para);
    
    NSLog(@"===>%@ %@", [NSThread currentThread], name);
    
    return NULL;
}

C和OC的桥接

  • __bridge只做类型转换,但是不修改对象(内存)管理权
  • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象
  • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC

线程和Runloop的关系

  • runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
  • runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。
  • runloop在第一次获取时被创建,在线程结束时被销毁。
  • 对于主线程来说,runloop在程序一启动就默认创建好了。
  • 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调