iOS底层探索之多线程(二)—线程和锁
回顾
在上一篇博客中,我们已经对进程和线程有了一定的了解了,那么本次博客将继续讲解!
在这里插入图片描述1. 线程的生命周期
在程序开发中有个名词——生命周期
,我们都知道APP
有生命周期,那么线程的生命周期是什么样子的呢?
- 线程生命周期
线程生命周期大致包括
5
个阶段:
-
新建
:通过创建线程的函数方法,创建一个新的线程。 -
就绪
:线程创建完成之后,调用start
方法,线程这个时候处于等待状态,等待CPU
时间分配执行。 -
运行
:当就绪的线程被调度并获得CPU
资源时,便进入运行状态,run
方法定义了线程的操作和功能。 -
阻塞
:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞
状态,比如sleep
、等待同步锁
,线程就从可调度线程池
移出,处于了阻塞
状态,这个时候sleep
到时、获取同步锁
,此时会重新添加到可调度线程池
。唤醒的线程不会立刻执行run
方法,它们要再次等待CPU
分配资源进入运行状态。 -
销毁
:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源。 -
线程生命周期大致流程图如下:
- 线程状态演练方法
@interface ViewController ()
@property (nonatomic, strong) NSThread *p_thread;
@end
/**
线程状态演练方法
*/
- (void)testThreadStatus{
NSLog(@"%d %d %d", self.p_thread.isExecuting, self.p_thread.isFinished, self.p_thread.isCancelled);
// 生命周期
if ( self.p_thread == nil || self.p_thread.isCancelled || self.p_thread.isFinished ) {
self.p_thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
self.p_thread.name = @"跑步线程";
[self.p_thread start];
}else{
NSLog(@"%@ 正在执行",self.p_thread.name);
//可以设置弹框 ---> 这里直接制空
[self.p_thread cancel];
self.p_thread = nil;
}
}
2. 线程池的运行策略
线程池运行策略
线程的工作执行,也是有一定的策略的,线程池的运行策略见下图:
线程池运行策略队列满了且正在运行的线程数量,小于最大线程数,则新进来的任务,会直接创建非核心线程来完成工作。
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当有任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于
corePoolSize
(核心线程数),那么马上创建核心线程
运行这个任务。
如果正在运行的线程数量大于或等于corePoolSize
,那么将这个任务放入队列。 - 如果这时候队列满了,而且正在运行的线程数量小于
maximumPoolSize
(最大线程数),那么还是要创建非核心线程立刻运行这个任务。 - 如果队列满了,而且正在运行的线程数量大于或等于
maximumPoolSize
,那么线程池饱和策略
将进行处理。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(超时)时,线程池会判断,如果当前运行的线程数大于
corePoolSize
,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize
的大小。
饱和策略
如果线程池中的队列满了,并且正在运行的线程数量已经大于等于当前线程池的最大线程数,则进行饱和策略的处理。
-
AbortPolicy
直接抛出RejectedExecutionExeception
异常来阻⽌系统正常运⾏ -
CallerRunsPolicy
将任务回退到调⽤者 -
DisOldestPolicy
丢掉等待最久的任务 -
DisCardPolicy
直接丢弃任务
3. 自旋锁和互斥锁
任务的执行速度的影响因素:
CPU
- 任务的复杂度
- 任务的优先级
- 线程的状态
优先级翻转:
-
IO
密集型(频繁的等待线程) -
CPU
密集型(很少等待) -
IO
比CPU
更容易得到优先级的提升 - 饿死:一直等不到执行,就丢弃了
- 调度:优先级和
CPU
的调度还有关系
优先级因素:
- 用户指定优先级 -->
threadPriority
// 主线程 512K
NSLog(@"%@ %zd K %d", [NSThread currentThread], [NSThread currentThread].stackSize / 1024, [NSThread currentThread].isMainThread);
NSThread *t = [[NSThread alloc] initWithTarget:self selector:@selector(eat) object:nil];
// 1. name - 在应用程序中,收集错误日志,能够记录工作的线程!
// 否则不好判断具体哪一个线程出的问题!
t.name = @"吃饭线程";
//This value must be in bytes and a multiple of 4KB.
t.stackSize = 1024*1024;
t.threadPriority = 1;
[t start];
threadPriority
threadPriority
替换成qualityOfService
(NSQualityOfService
)
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
- 等待的频繁度
- 长时间不执行(也会提升优先级)
下图是:一个线程的经典案例
线程经典案例
车票售卖系统,是线程工作执行的经典案例,如果是多个窗口卖票,会出现资源抢夺的情况,如果 A窗口卖了一张票,B窗口不知道,或者同一时间 AB窗口卖同一张票,这样就会出现问题,所有锁的意义重大了。
自旋锁
是一种用于保护多线程共享资源
的锁,与一般互斥锁
(mutex
)不同之处在于当自旋锁
尝试获取锁时,以忙等待(busy waiting
)的形式不断地循环检查锁是否可用;当上一个线程的任务没有执行完毕的时候(被锁住
),那么下一个线程会一直等待
(不会睡眠);当上一个线程的任务执行完毕,下一个线程会立即执行。
在多CPU
的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。
自旋锁
:OSSpinLock、dispatch_semaphore_t
互斥锁
当上一个线程的任务没有执行完毕的时候(被锁住
),那么下一个线程会进入睡眠状态
等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪
)。
互斥锁
:pthread_mutex、@ synchronized、NSLock、NSConditionLock、NSCondition、NSRecursiveLock
自旋锁和互斥锁的特点
-
自旋锁
会忙等,所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。 -
互斥锁
会休眠,所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作,直到被锁资源释放锁。此时会唤醒休眠线程。
自旋锁优缺点
- 优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,CPU时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
- 缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。
原子属性和非原子属性
OC
在定义属性时有nonatomic
和atomic
两种选择,默认为atomic
属性
-
atomic
:原子属性,为setter
方法加自旋锁(即为单写多读) -
nonatomic
:非原子属性,不会为setter方法加锁
nonatomic
和atomic
的对比
-
atomic
:线程安全,需要消耗大量的资源; -
nonatomic
:非线程安全,适合内存小的移动设备。
平时开发需要注意
- 如非需抢占资源的属性(如购票,充值),所有属性都声明为
nonatomic
。 - 尽量避免多线程
抢夺
同一块资源。 - 尽量将
加锁
、资源抢夺
的业务逻辑交给服务器端处理,减小移动客户端的压力。
atomic
底层实现自旋锁
我们在探索类的本质时,对于类的属性的setter
方法,系统会有一层objc_setProperty
的封装(libobjc.dylib源码)
底层会调用
reallySetProperty
方法,在该方法的实现中,针对原子属性,添加了spinlock
锁
- objc_setProperty_atomic_copy
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
- reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
Spinlock
是Linux
内核中提供的一种比较常见的锁机制,自旋锁是原地等待的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地打转(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗CPU
资源)。
纠正一下
atomic
只是原子属性的一个标识符,所以atomic
并不是自旋锁,底层是通过Spinlock
实现自旋锁。
线程和Runloop的关系
-
runloop
与线程是一一对应的,一个runloop
对应一个核心的线程,为什么说是核心的,是因为runloop
是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局
的字典里。 -
runloop
是来管理线程的,当线程的runloop
被开启后,线程会在执行完任务后进入休
眠状态,有了任务就会被唤醒去执行任务。 -
runloop
在第一次获取时被创建,在线程结束时被销毁。 - 对于主线程来说,
runloop
在程序一启动就默认创建好了。 - 对于子线程来说,
runloop
是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意
:确保子线程的runloop
被创建,不然定时器不会回调。
4. iOS技术方案
多线程有
Pthread
、NSThread
、GCD
、NSOperation
等方案。
iOS技术方案如下图:
iOS技术方案
关于多线程的更多信息,可以去苹果文档去看看
Threading Programming Guide
更多内容持续更新
🌹 喜欢就点个赞吧👍🌹
🌹 觉得有收获的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹
🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹