iOS 多线程原理

前言

iOS开发过程中,线程的处理是我们不可绕开的技术话题。比如pthreadNSThreadGCDNSOperation,其中iOS开发中GCDNSOperation是我们最常用。在研究这些之前,我们先来了解一些多线程方面的概念。

1.线程和进程

1.1定义

  • 线程的定义
    • 线程是进程的基本执⾏单元,⼀个进程的所有任务都在线程中执⾏
    • 进程要想执⾏任务,必须得有线程,进程⾄少要有⼀条线程
    • 程序启动会默认开启⼀条线程,这条线程被称为主线程UI线程
  • 进程的定义
    • 进程是指在系统中正在运⾏的⼀个应⽤程序
    • 每个进程之间是独⽴的,每个进程均运⾏在其专⽤的且受保护的内存空间内
    • 通过活动监视器可以查看Mac系统中所开启的进程
      活动监视器查看mac进程

      如上图Mac活动监视器中,罗列出了当前运行的进程,各个进程之间相互独立运行,每个进程内会有多个线程在运行。

1.2进程与线程的关系

  • 地址空间:同⼀进程的线程共享本进程的地址空间,⽽进程之间则是独⽴的地址空间。
  • 资源归属:同⼀进程内的线程共享本进程的资源,如内存I/Ocpu等,但是进程之间的资源是相互独⽴的。
  • 进程和线程的关系:
    • ⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进程都死掉。所以多进程要⽐多线程健壮
    • 进程切换时,消耗的资源⼤效率低。所以涉及到频繁的切换时,使⽤线程要好于进程。同样如果要求同时进⾏并且⼜要共享某些变量的并发操作,只能⽤线程不能⽤进程。
    • 执⾏过程中每个独⽴的进程有⼀个程序运⾏的⼊⼝顺序执⾏序列程序⼊⼝。但是线程不能独⽴执⾏,必须依存在应⽤程序中,由应⽤程序提供多个线程执⾏控制。
    • 线程是处理器调度的基本单位,但是进程不是。
    • 线程没有地址空间,线程包含在进程地址空间中。

1.3 多线程的意义

在开发过程中,多线程我们一直在面对着,但是我们明白多线程能给开发带来什么好处吗?通过以下的案例分析看看:

案例分析

上面的案例循环十万次创建内容,因为没有进行异步的处理,所以这个操作一直在主线程进行。这个过程一共耗时20秒左右,这样子给APP带来卡顿,极大地影响了用户体验。

为了解决以上的问题,我们需要对循环中的任务进行异步处理。如果一个事务很复杂,比较耗时,可以将一个大的事务拆分成多个小的事务进行并发处理,这样可以节省时间,并且不会影响用户的体验。

多线程的优缺点

  • 优点:
    1.能适当提⾼程序的执⾏效率
    2.能适当提⾼资源的利⽤率(如CPU内存

    1. 线程上的任务执⾏完成后,线程会⾃动销毁
  • 缺点
    1.开启线程需要占⽤⼀定的内存空间(默认情况下,每⼀个线程都占512KB
    2.如果开启⼤量的线程,会占⽤⼤量的内存空间降低程序的性能
    3.线程越多,CPU在调⽤线程上的开销就越⼤
    4.程序设计更加复杂,⽐如线程间的通信、多线程的数据共享

1.4时间片概念

开启过多的线程也会导致性能的下降,这里涉及到时间片的概念。多线程的执行是CPU快速的在多个线程之间进行切换。线程数过多,CPU会在多个线程之间切换,销毁大量的CPU资源,反而导致执行效率的下降

  • 时间片的概念CPU在多个任务直接进⾏快速的切换,这个时间间隔就是时间⽚。(单核CPU)同⼀时间,CPU只能处理1个线程,换⾔之,同⼀时间只有 1个线程在执⾏
  • 多线程同时执⾏:是CPU快速的在多个线程之间的切换CPU调度线程的时间⾜够快,就造成了多线程的同时执⾏的效果
  • 线程数⾮常多的情况CPU会在N个线程之间切换,消耗⼤量的CPU资源,每个线程被调度的次数会降低,线程的执⾏效率降低

2.iOS内存五大区

我们知道内存主要分为五大区,分别是:栈区堆区全局区常量区代码区,详细见下图:

内存五大区

2.1栈区(stack)

  • 特点
    • 栈是系统数据结构,其对应的进程或者线程是唯一
    • 栈是向低地址扩展的数据结构
    • 栈是一块连续的内存区域,遵循先进后出FILO)原则
    • 栈的地址空间在iOS中是以0X7开头
    • 栈区一般在运行时分配
  • 存储内容
    • 栈区是由编译器自动分配并释放的,主要用来存储局部变量
    • 函数的参数,例如函数的隐藏参数id selfSEL _cmd
  • 优缺点
    • 优点:因为栈是由编译器自动分配并释放的,不会产生内存碎片,所以快速高效
    • 缺点:栈的内存大小有限制数据不灵活
      iOS主线程栈大小是1MB,其他主线程是512KBMAC只有8M

注意:传入函数的参数值、函数体内声明的局部变量等,由编译器自动分配释放,通常在函数执行结束后就释放了。(不包括static修饰的变量,static意味该变量存放在全局/静态区)。

2.2堆区(heap)

  • 特点
    • 堆是向高地址扩展的数据结构
    • 堆是不连续的内存区域,类似于链表结构便于增删,不便于查询),遵循先进先出FIFO)原则
    • 堆的地址空间在iOS中是以0x6开头,其空间的分配总是动态
    • 堆区的分配一般是在运行时分配
  • 存储内容
    • 堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收
    • OC中使用alloc或者使用new开辟空间创建对象
    • C语言中使用malloccalloc、realloc分配的空间,需要free释放
  • 优缺点
    • 优点:灵活方便,数据适应面广泛
    • 缺点:需手动管理速度慢、容易产生内存碎片

注意:当访问堆中内存时,一般需要先通过对象读取到栈区指针地址,然后通过指针地址访问堆区。因为现在iOS基本都使用ARC来管理对象,所以也不需要手动释放

2.3全局区(静态区)(BSS段)

  • BSS段(bss segment):通常是指用来存放程序中未初始化的或者初始值为0全局变量的一块内存区域BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配

  • 数据段:数据段(data segment)通常是指用来存放程序中已初始化全局变量的一块内存区域,数据段属于静态内存分配

  • 全局区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序运行过程中,此内存中的数据一直存在程序结束后由系统释放,主要存放

    • 未初始化的全局变量和静态变量,即BSS区(.bss)
    • 已初始化的全局变量和静态变量,即数据区(.data)
  • static修饰的变量会成为静态变量,该变量的内存由全局/静态区编译阶段完成分配,且仅分配一次
  • static可以修饰局部变量也可以修饰全局变量

2.4常量区(数据段)

  • 常量区是编译时分配的内存空间,在iOS中一般以0x1开头,在程序结束后由系统释放
  • 通常是指用来存放程序中已经初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段读写数据段字符串常量等,是放在只读数据段中,结束程序时才会被收回

2.5代码区(text段)

  • 代码区是编译时分配主要用于存放程序运行时的代码,代码会被编译成二进制存进内存
  • 代码区需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作。

3.线程的生命周期

线程的生命周期包含5个阶段,包括:新建就绪运行阻塞销毁。见下图:

线程的生命周期

  • 新建:就是刚通过alloc,创建出来的线程;
  • 就绪:就是调用的线程的start方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep等待同步锁线程就从可调度线程池移出,处于了阻塞状态,这个时候sleep到来、获取同步锁,此时会重新添加到可调度线程池。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入就绪状态
  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止出现异常导致结束,那么线程就要被销毁,释放资源

4.线程池的运行策略

线程池的运行策略,见下图:

线程池运行策略

由上图可知:队列满正在运行的线程数量小于最大线程数,则新进入的任务,会直接创建非核心线程工作。

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  • 当有任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于corePoolSize核心线程数),那么马上创建核心线程运行这个任务;
    • 如果正在运行的线程数量大于等于corePoolSize,那么将这个任务放入队列
    • 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize(最大线程数),那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于等于maximumPoolSize,那么线程池饱和策略将进行处理。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 当一个线程无事可做,超过一定的时间(超时)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

线程池的饱和策略

如果线程池中的队列满了,并且正在运行的线程数量已经大于等于当前线程池的最大线程数,则进行饱和策略的处理。

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

5.自旋锁和互斥锁

5.1.自旋锁

一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
注意:在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

自旋锁:OSSpinLockdispatch_semaphore_t

5.2互斥锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态就绪)。
互斥锁:pthread_mutex@synchronizedNSLockNSConditionLockNSConditionNSRecursiveLock

5.3自旋锁和互斥锁的特点

  • 自旋锁会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环访问是否已经解锁,直到被锁资源释放锁。
  • 互斥锁会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程
  • 自旋锁优缺点
    • 优点:因为自旋锁不会引起调用者睡眠,所以不会进行线程调度CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
    • 缺点:自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用

5.4原子属性和非原子属性

  • OC在定义属性时有nonatomicatomic两种选择,默认为atomic属性
    • atomic原子属性,为setter方法加自旋锁(即为单写多读
    • nonatomic非原子属性,不会为setter方法加锁
  • nonatomicatomic的对比
    • atomic线程安全,需要消耗大量的资源
    • nonatomic非线程安全,适合内存小的移动设备。

注意:

  • 如非需抢占资源的属性(如购票,充值),所有属性都声明为nonatomic
  • 尽量避免多线程抢夺同一块资源。
  • 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。

5.5原子属性和非原子属性的原理

在探索类的本质时,对于类的属性的setter方法,系统会有一层objc_setProperty的封装(libobjc.dylib源码)。见下图:

objc_setProperty方法

底层会调用reallySetProperty方法,在该方法的实现中,针对原子属性,添加了spinlock锁,见下图:
reallySetProperty

SpinlockLinux内核中提供的一种比较常见的锁机制,自旋锁是原地等待的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地打转(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗CPU资源)。
注意:atomic只是原子属性,一个标识符,所以atomic并不是自旋锁,底层是通过Spinlock实现自旋锁。

6.iOS技术方案

iOS技术方案

补充:锁的详细剖析会在下一篇文章中

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 基本概念 线程 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行 进程要想执行任务,必须得有线程,进程至...
    xxxxxxxx_123阅读 278评论 0 0
  • 本文的目的在于了解进程、线程、多线程、线程池等的基本概念及原理 线程 和 进程 线程和进程的定义 线程 线程时进程...
    辉辉岁月阅读 154评论 0 0
  • 了解多线程的原理,保证App的质量。 一、进程、线程与队列 1:进程 进程 是指在系统中正在运行的一个应用程序,如...
    土豆骑士阅读 741评论 0 2
  • 多线程原理 线程和进程的关系和区别 1、线程定义 线程是进程的基本执行单元,一个进程的所有任务都要在线程中执行 进...
    荒漠现甘泉阅读 575评论 0 1
  • 进程 进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存中。 线...
    瞬间完善阅读 498评论 0 2