11--多线程01--多线程概念(线程和进程)

[TOC]

一、 线程和进程

1.1 线程的定义

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

1.2 进程的定义

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

1.3 iOS开发为什么是单进程的

  • 沙盒-单进程数据更加隐私、安全
  • 进程切换,消耗资源大

1.4 线程和队列的关系

  • 线程和队列是两个完全独立的概念,线程是由系统分配、系统调度,队列是用来管理任务,开发者可以管理任务的串行、并行、优先级、先后顺序;
  • 开发者管理任务队列,系统根据队列的属性,来给队列中的任务分配线程;
  • 队列是提供给开发者使用的抽象概念,线程是由系统调度的真实对象,开发者可以通过管理队列来指定系统分配、调度线程的数量、顺序等。
- 任务执行速度:CPU、线程、队列、任务的复杂度、任务的优先级
- 大量的临时变量:使用 autorealease 处理

1.5 线程和进程的关系和区别

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间
  • 资源拥有:同一进程内的线程共享进程的资源,如内存、I/O、cpu等,但是进程之间的资源是独立的
  • 健壮性:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都会死掉。多以多进程要比多线程健壮
  • 并发操作:进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能使用线程不能用进程
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存于应用程序中,由应用程序提供多个线程执行控制
  • 调度单位:线程是处理器调度的基本单位,但进程不是

1.6 进程和线程与堆和栈的关系

  • 进程的地址空间
    每个进程的地址空间是独立的,是操作系统把物理内存映射到进程的虚拟地址上。
    进程只能访问自己的地址空间,不能访问别的进程的地址空间。
    通俗的理解:如果有a.exe和b.exe同时在系统中运行,那么a和b都可以有0x00000000-0xffffffff的虚拟地址空间(不考虑操作系统占用等因素)。
    假如a b都有1个变量在地址 0x12345678处,看起来地址一样,实际上在物理内存中不是一个地方。
  • 进程是线程的容器,windows调度运行的单位是线程,一个进程至少有1个主线程。一个进程内所有的线程都处于同一个虚拟地址空间。
  • 进程初始化的时候,系统会在进程的地址空间中创建一个堆,叫进程默认堆。进程中所有的线程共用这一个堆。当然,可以增加1个或几个堆,给不同的线程共同使用或单独使用。
  • 创建线程的时候,系统会在进程的地址空间中分配1块内存给线程栈,通常是1MB。线程栈是独立的,不共享iOS中是512KB

1.7 补充内存相关知识

  • 内存五大分区
    1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。new出来的放在这里。
    3、全局区(静态区):(static)全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
    4、文字常量区:常量字符串就是放在这里的。程序结束后由系统释放
    5、程序代码区:存放函数体的二进制代码。

  • 栈和堆的区别

    • 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
    • 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1MB,64bits的Linux默认10MB;
    • 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
    • 分配方式不同
      堆都是动态分配的,没有静态分配的堆。
      栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。
      栈的动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,栈的动态分配是由操作系统进行释放,无需我们手工实现。
    • 分配效率不同
      栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
      堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。

二、 多线程

2.1 多线程的意义

  • 优点
    • 能适当提高程序的执行效率
    • 能适当提高资源的利用率(CPU、内存)
    • 线程上的任务执行完成后,线程会自动销毁
  • 缺点
    • 开启线程需要占用一定的内存空阿金(默认情况下,每一个线程都占 512 KB)
    • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
    • 线程越多,CPU 在调用线程上的开销就越大
    • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

2.2 多线程的原理

CPU 在单位时间片里快速在各个线程之间切换

  • 单线程
    单核 CPU

    单线程
        任务1
        任务2
        任务3
    
    • 其本质也是单线程,一次只能执行一个任务
    • 单核 CPU的多线程是通过 任务1执行一部分再执行任务2再切换任务3,这种方式达到多线程的效果
    • 不停切换、时间片、共赢、达到利益最大化
  • 多线程
    多核 CPU

    线程1
        任务1
    线程2
        任务2
    线程3
        任务3
    

2.3 多线程技术方案

  • pthread

    • 一套通用的多线程API
    • 适用于 Unix/Linux/Windows 等系统
    • 跨平台/可移植
    • 使用难度大
  • NSThread

    • 使用更加面向对象
    • 简单易用,可直接操作线程对象
  • GCD

    • 旨在替代 NSTread 等线程技术
    • 充分利用设备的多核
  • NSOperation

    • 基于GCD(底层是GCD)
    • 比 GCD 多了一些更简单使用的功能
    • 使用更加面向对象

三、线程、线程池、生命周期、安全、通讯、runloop

3.1 线程的生命周期

  • 新建:Start,创建线程,调用start进入就绪状态

  • 就绪:Runnable,CPU调度当前线程,进入运行状态

  • 运行:Running,任务执行完毕,退出线程并销毁,CPU调度当前线程

  • 阻塞:Blocked,调用 sleep方法/等待同步锁/从可调度线程池移出

  • 死亡:Dead,任务执行完成、强制退出

  • 可调度线程池

    • 当前线程
    • 其他线程
    • CPU调度的线程是从线程池中获取
  • 正常流程和堵塞流程

    • 正常流程:新建->就绪->运行->死亡
    • 堵塞流程:新建->就绪->运行->堵塞->就绪->...->堵塞
      image.png

3.2 线程池的原理

  • 参数解释
    • corePoolSize:线程池的基本大小(核心线程池大小)
    • maxinumPoolSize:线程池的最大大小
    • keepAliveTime:线程池中超过corePoolSize数目的空闲线程的最大存活时间
    • unit:keepAliveTime参数的时间单位
    • workQueue:任务阻塞队列
    • threadFactory:新建线程的工厂
    • handler:当提交的任务数超过maxnumPoolSize与workQueue之和时,任务会交给RejectedExecutionHandler来处理
  • 【原理】线程池大小小于核心线程池大小
    • 创建线程执行任务,流程和线程的生命周期一样
  • 【原理】线程池大小不小于核心线程池大小
    • 线程池判断工作队列未满
      将任务push进队列
    • 线程池判断工作队列已满
      • maxinumPoolSize>corePoolSize,将创建新的线程来执行任务
      • 交给报策略去处理
        1. Abort策略:默认策略,新任务提交时直接抛出未检查的异常 RejectedExecution,该异常可由调用者捕获
        2. CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务会退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务
        3. Discard策略:新提交的任务被抛弃。
        4. DiscardOldest策略:队列的是“对头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)

3.3 线程和runloop的关系

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

3.4 线程安全和线程通讯

  • atomicnonatomic的区别

    • 属性定义
      • nonatomic:非原子属性,适合内存小的移动设备
      • atomic:原子属性(线程安全),针对多线程设计,默认值,需要消耗大量的资源。atomic只保证了写安全,但对于可变数组这类容器中的元素的读写安全并不能保证。
    • 线程安全解释
      • 保证同一时间只有一个线程能够写入
      • atomic本身就有一把锁(自旋锁)
      • 单写多读:单个线程写入,多个线程可以读取
    • iOS开发的建议
      • 所有属性都声明为 nonatomic
      • 尽量避免多线程抢夺一块资源
      • 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
  • 互斥锁小结

    • 保证锁内的代码,同一时间,只有一条线程能够执行
    • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差
    • 互斥锁参数
      • 能够加锁的任意 NSObject 对象
      • 注意:锁对象一定要保证所有的线程都能够访问
      • 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象

四、iOS中常用多线程技术

4.1 pthread

pthread_create 创建线程

  • 参数:

    1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 _t / Ref 结尾
      同时不需要 *
    2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,C 使用的)
    3. 线程要执行的函数地址
      void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
      (*): 函数名
      (void *): 参数类型,void *
    4. 传递给第三个参数(函数)的参数
      • 返回值:C 语言框架中非常常见
        int
        0 创建线程成功!成功只有一种可能
        非 0 创建线程失败的错误码,失败有多种可能!
        // 1: pthread
        pthread_t threadId = NULL;
        //c字符串
        char *cString = "HelloCode";
    
        int result = pthread_create(&threadId, NULL, pthreadTest, cString);
        if (result == 0) {
            NSLog(@"成功");
        } else {
            NSLog(@"失败");
        }
    
        void *pthreadTest(void *para){
            // 接 C 语言的字符串
            //    NSLog(@"===> %@ %s", [NSThread currentThread], para);
            // __bridge 将 C 语言的类型桥接到 OC 的类型
            NSString *name = (__bridge NSString *)(para);
            
            NSLog(@"===>%@ %@", [NSThread currentThread], name);
            
            return NULL;
        }
    
4.2 NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
/**
 1. 循环的执行速度很快
 2. 栈区/常量区的内存操作也挺快
 3. 堆区的内存操作有点慢
 4. I(Input输入) / O(Output 输出) 操作的速度是最慢的!
 * 会严重的造成界面的卡顿,影响用户体验!
 * 多线程:开启一条线程,将耗时的操作放在新的线程中执行
 */
- (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");
}
4.3 GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self threadTest];
});
4.4 NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
    [self threadTest];
}];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容