学习重点
进程与线程的定义
多线程及其原理
线程的生命周期
可调度线程池以及饱和策略
自旋锁与互斥锁
1. 进程以及线程的定义
1.1 进程
进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内,通过”活动监视器”可以查看Mac
系统中开启的进程。
如下图所示:
1.2 线程
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程想要执行任务,必须得有线程,进程至少要有一条线程,程序启动默认会开启一条线程,这条线程被称为主线程或UI
线程。
1.3 进程与线程的关系
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有:同一进程内的线程共享本进程的资源,如内存、I/O
、CPU
等,但是进程之间的资源是独立的。
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都会死掉。所以多进程要比多线程健壮。
- 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程而不能用进程。
- 执行过程:每个独立的进程有一个程序运行的入口,顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,而应用程序提供多个线程执行控制。
- 线程是处理器调度的基本单位,而进程不是。
- 线程没有地址空间,线程包含在进程地址空间中。
2. 多线程
2.1 为何使用多线程
在iOS
开发中,我们不可能使用单线程来开发,因为我们在开发过程中,经常需要编写一些耗时操作的代码(例如:网络请求、文件上传下载、蓝牙的操作等),如果仅是单线程开发(仅有主线程),那么必将发生UI
上的卡顿,将会极大的影响用户体验,例如在以下界面中编写如下代码:
- (IBAction)networkOperation:(id)sender {
NSLog(@"开始耗时操作");
for (int i = 0; i < 100 * 1000; i++) {
int a = i * i;
NSString *str = @"----";
NSLog(@"正在进行耗时操作:%@ %d", str, a);
}
NSLog(@"耗时操作执行完毕");
}
当按钮点击时,将执行其中的耗时操作,你会发现,在控制台打印输出信息的过程中(有将近20
秒的时长),你是无法做任何其他操作的(例如:滑动textView
文本),因此单线程是无法满足我们的需求的。
2.2 多线程的优缺点
2.2.1 多线程的优点
-
能适当提高程序的执行效率。
-
能适当提高资源的利用率(
CPU
,内存)。 -
线程上的任务执行完成后,线程会自动销毁。
2.2.2 多线程的优点
-
开启线程需要占用一定的内存(默认情况下,每一个线程都占
512KB
)。 -
如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
-
线程越多,
CPU
在调用线程上的开销就越大。 -
程序设计更加复杂,比如线程间的通信,多线程的数据共享。
2.3 多线程的原理
任务的执行依赖于线程,那么多线程则表示多个任务同时执行,而CPU
同一时间只能执行一个任务,那么多线程是怎么来的呢?
CPU
在执行任务的时候,实际上是多个线程之间快速切换执行,这个时间间隔是很小的,小到我们感觉多个线程是同时执行的。
所以单核CPU
的多线程就是快速的在多个线程中不断的切换调用,已达到所有线程都在同时进行的效果,但是现在很多设备都是双核,四核或者八核的,相当于多个CPU
同时处理,更能加快系统的运行处理速度,这才是真正意义上的多线程。
上述所说的那段很短的时间也称之为时间片
(通常在10-100
毫秒左右),其概念就是CPU
在多个任务之间进行快速的切换,这个时间间隔就是时间片
。如果线程数非常多,CPU
会在N
个线程之间切换,消耗大量的CPU
资源,每个线程被调度的次数会降低,线程的执行效率降低。
3. 线程的声明周期、可调度线程池以及饱和策略
3.1 线程的声明周期
线程的生命周期一共分为五个部分分别是:新建,就绪,运行,阻塞以及死亡。由于CPU
需要在多条线程中切换,因此线程状态也会在多次运行和阻塞之间切换,如下图所示:
-
新建状态:被创建,在内存中,但不在可调度线程池中。
-
就绪状态:调用线程的
start
方法后,在可调度池中,可以执行任务。 -
运行状态:在可调度线程池中,正在执行任务。
-
阻塞状态:被移除可调度线程池,在内存中,不能执行任务。
-
死亡状态:被释放。
3.2 可调度线程池
线程的运行策略,如下图所示:
-
首先,先判断线程池中线程的数量是否超过核心线程数(
corePoolSize
),如果没有超过核心线程数,就创建新的线程去执行任务; -
否则,判断任务队列是否已满,如果没有满,就将任务添加到任务队列中;
-
否则,再判断如果创建一个线程后,线程数是否会超过最大线程数(
maximumPoolSize
),没有超过最大线程数,就创建一个新的线程来执行任务; -
否则,执行饱和策略
3.3 饱和策略
饱和策略共有下面四种,这四种策略均实现了RejectedExecutionHandler
接口。
-
AbortPolicy
直接抛出RejectedExecutionExeception
异常来阻止系统正常运行。 -
CallerRunsPolicy
将任务回退到调用者。 -
DisOldestPolicy
丢掉等待最久的任务。 -
DisCardPolicy
直接丢弃任务。
4. 锁
在计算机科学中,锁是一种同步机制,用于在存在多线程的环境中实施对资源的访问限制。你可以理解成它用于排除并发的一种策略!你可以理解为为了防止多线程访问下资源的抢夺,保持线程同步的方式。
使用任何锁都需要消耗系统资源(内存资源和CPU
时间),这种资源消耗可以分为两类:
-
建立锁所需要的资源
-
当线程被阻塞时所需要的资源
4.1自旋锁
自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex
)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting
)的形式不断地循环检查锁是否可用。当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU
的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。
-
优点:自旋锁的优点在于,因为自旋锁不会引起调用栈睡眠,所以不会进行线程调度,
CPU
时间片轮转等耗时操作,所以如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁 -
缺点:自旋锁一直占用
CPU
,在未获得锁的情况下,一直运行(自旋),所以占用CPU
,如果不能在短的时间内获得锁,这无疑会使CPU
效率降低,自旋锁不能实现递归调用。
常用自旋锁:atomic
、OSSpinLock
、dispatch_semaphore_t
。
4.2 互斥锁
当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
互斥锁保证了锁内的代码,同一时间,只有一条线程能够执行,使用互斥锁时应注意锁定范围应该尽量小,锁定范围越大,其效率越差。
常用互斥锁:pthread_mutex
、@ synchronized
、NSLock
、NSConditionLock
、NSCondition
、NSRecursiveLock
。
4.3 atomic与nonatomic
4.3.1 两者的定义及区别
我们在使用O
C开发iOS
应用程序时,定义OC
类的属性时可以选择atomic
或者nonatomic
,其中:
-
atomic
: 原子属性,为setter
方法加自旋锁(即为单写多读)。 -
nonatomic
: 非原子属性,不为setter
方法加锁。
这两种方式的区别如下:
-
atomic
: 线程安全,需要消耗大量的资源。 -
nontomic
: 非线程安全,适合内存小的移动设备。
4.3.2 使用建议
在iOS
开发中,我们一般都遵循以下的原则进行程序代码的编写:
-
如非需抢占资源的属性(如购票,充值等),所有的属性都声明为
nonatomic
。 -
尽量避免多线程抢夺同一块资源。
-
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,以减小移动客户端的压力。
4.3.3 atomic原理探究
首先我们都知道在属性的setter
方法底层,实际上调用了objc_setProperty
函数,其代码如下所示:
在这个函数中调用了reallySetProperty
函数,其代码如下所示:
在这个函数中,可以发现实际上atomic
实际上只是一个标识符,而添加自旋锁的功能实际上是由spinlock_t
类型变量slotlock
所完成的,其定义如下所示:
spinlock
就是系统所提供的一种自旋锁。