线程的基本概念
线程和进程
进程是一个正在运行的程序,进程之间的内存空间是隔离的。
线程是CPU调度的基本单位,同一个进程的各个线程共享内存空间。一个进程至少有一个线程。
进程小知识
我们申请内存的时候,并不是直接问操作系统要内存,而是操作系统一次性给一批内存,然后不够用的时候再申请。
获取pagesize的方法,模拟器,macOS4KB,真机16KB。
#import <mach/mach.h>
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
mach_port_t host_port;
vm_size_t pagesize;
host_port = mach_host_self();
host_page_size(host_port, &pagesize);
NSLog(@”%ld", pagesize);
}
在macOS下可以直接使用PAGESIZE获取分页大小。
线程小知识
线程栈内存 主线程默认1MB,子线程512KB。
堆栈溢出 其实大部分都是栈溢出。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self count:0];
});
// [self count:0];
}
- (void)count:(NSInteger)count{
NSLog(@"%ld", count);
[self count:count + 1];
}
尾递归优化
- (NSInteger)count:(NSInteger)num{
if (num == 10000){
return 10000;
}
return [self count:(num + 1)];
}
不完全尾递归
函数结束没有回收内存资源。
栈内存效率比堆内存高的原因 堆内存有一张全局的表,需要内存回收利用。栈内存不用回收。
CPU和寄存器
Arm64一共有34个寄存器。31个通用寄存器,SP,PC, CPSR。
31个通用分别是X0,X30。如果32位运算,则为W0,W30。
FP(X29)栈底。
LR(X30)函数返回的地址。
SP 栈顶。
PC 保存下一条指令的地址。
CRSR状态寄存器。
并发和并行
一个CPU核心可以也可以通过快速的在不同的任务之间切换,给人的感受就是同时在执行,这就是并发。
并行是在任意时刻,多个处理核心在同时的处理任务。单个核心无法实现并行。并行数是跟CPU核心数密切相关的。
// 正在工作的核心数
NSLog(@"%ld", [NSProcessInfo processInfo].activeProcessorCount);
// 一共有多少个核心数
NSLog(@"%ld", NSProcessInfo.processInfo.processorCount);
超线程
传统的CPU在设计的时候,每次只能执行一条汇编代码。比如CPU支持加法,减法运算。
当CPU在执行加法运算的时候,减法运算器就被闲置了。因此为了提高CPU的利用率,发明了超线程技术,即2个线程共用一个物理核心,让第二个线程执行的指令和第一个线程不一致时,可以立刻执行。当同时执行加法运算,则需要等待。
线程池
主线程默认是1M,子线程默认是512KB的内存。开启线程需要90微秒。
GCD缓存了64条线程,这样下次开辟线程的时候直接从线程池取,可以节省开辟线程所用的时间。
线程的生命周期
- 创建
- Runnable就绪状态:此时线程可以被调度,但是目前CPU没空。
- Running运行状态:CPU正在调度该线程。
- Blocking阻塞:此时CPU无法调度该线程。比如sleep会导致阻塞。sleep时间到了,又会到就绪状态。
- Dead死亡状态:此时程序执行结束或者因为程序出错导致闪退。
同步和异步
同步:在当前线程阻塞执行,不会开启子线程。
线程面试题
题目1:如下输出是啥。
dispatch_queue_t queue = dispatch_queue_create("test gcd", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{
NSLog(@"4");
});
NSLog(@"5");
});
NSLog(@"6");
题目2:如下输出是啥。
dispatch_queue_t queue = dispatch_queue_create("test gcd", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
// 如果改成async呢
dispatch_sync(queue, ^{
NSLog(@"4");
});
NSLog(@"5");
});
NSLog(@"6");
题目3:
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_sync(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
dispatch_async(queue, ^{
NSLog(@"5");
});
dispatch_async(queue, ^{
NSLog(@"6");
});
// 312456
// 3肯定是在4前面。56肯定在4后面。12可以在任意位置。
线程面试题-答案
题目1
输出16245。(一般情况下,2会比6后输出。但是2也可能会比6先输出。)
面试回答参考:
1是第一个输出的。
其中2和6的顺序不确定,但是大部分是6先输出。因为开启异步线程和当前线程是独立的两个现场,没有顺序关系。
接着是245顺序输出。输出2之后,由于queue是并发队列,因此dispatch_sync会等待4输出完毕,再接着输出5.
题目2
输出162,然后死锁。
面试回答参考:输出1和6。因为创建的队列是同步队列,所以输出2之后,会等待任务3执行结束,而任务3必须等其它任务都执行结束才能开始。所以2一直等待3,3一直无法开始执行,从而导致死锁。同样,6和2是两个独立的线程,无法保证先后顺序,大部分是先6后2.
如果改成async,则不会死锁。此时输出2之后,会直接输出5然后输出4。
死锁
在当前线程同步的向当前任务所在的串行队列添加任务就会死锁。
dispatch_queue_t queue = dispatch_queue_create("q", NULL);
dispatch_sync(queue, ^{
dispatch_sync(queue1, ^{
NSLog(@"1");
});
});
栅栏函数
对于自定义并发队列,dispatch_barrier_async会等待之前的任务结束,1和2结束之后才会执行3,3之后执行4和5。
dispatch_queue_t queue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"3");
});
dispatch_async(queue, ^{
NSLog(@"4");
});
dispatch_async(queue, ^{
NSLog(@"5");
});
如果将上面的队列改成dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
。则所有的任务都是异步执行的。主队列不允许异步执行栅栏函数。因为系统,第三方SDK如果也进行栅栏操作,如果可以阻塞,可能会导致应用卡死。
group
// 创建group
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0 );
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"1");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_group_leave(group);
});
// 等价于上面的enter和leave
dispatch_group_async(group, queue, ^{
NSLog(@"3");
});
// 必须等之前所有添加到group的任务结束,才会执行Block里面的代码。
dispatch_group_notify(group, queue, ^{
NSLog(@"4");
});
// 5的执行顺序和之前的任务没有关系
dispatch_async(queue, ^{
NSLog(@"5");
});
信号量
可以控制并发量。
当等于0的时候,dispatch_semaphore_wait
会阻塞。一直等到信号量大于0,然后减1,继续执行之后的代码。
dispatch_semaphore_signal
对信号量加1。
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
sleep(1);
NSLog(@"1");
// 信号加1
dispatch_semaphore_signal(sem);
});
NSLog(@"2");
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"3");
dispatch_async(queue, ^{
sleep(1);
NSLog(@"4");
dispatch_semaphore_signal(sem);
});
NSLog(@"5");
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
NSLog(@"6");
// 阻塞
dispatch_semaphore_t sp = dispatch_semaphore_create(0);
dispatch_semaphore_wait(sp, DISPATCH_TIME_FOREVER);
NSLog(@"123");
// Crash
dispatch_semaphore_t sp = dispatch_semaphore_create(2);
dispatch_semaphore_wait(sp, DISPATCH_TIME_FOREVER);
NSLog(@"123");
dispatch_source_t
dispatch_once
@implementation User
+ (instancetype)sharedInstance{
static dispatch_once_t onceToken;
static User *user;
dispatch_once(&onceToken, ^{
user = [User new];
});
return user;
}
@end
原理
dispatch_once
内部调用dispatch_once_f
。
void
dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
// 第三个参数是获取block的invoke
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
#endif
dispatch_once_f
内部调用dispatch_once_f
。
DISPATCH_NOINLINE
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
dispatch_once_gate_t l = (dispatch_once_gate_t)val;
#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
if (likely(v == DLOCK_ONCE_DONE)) {
return;
}
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
if (likely(DISPATCH_ONCE_IS_GEN(v))) {
return _dispatch_once_mark_done_if_quiesced(l, v);
}
#endif
#endif
if (_dispatch_once_gate_tryenter(l)) {
return _dispatch_once_callout(l, ctxt, func);
}
return _dispatch_once_wait(l);
}
锁
属性的atomic
get方法的实现
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot; // 如果非atomic,直接返回
// Atomic retain release world
// 如果atomic,则先加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
set方法的实现
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
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) {
// 非atomic
oldValue = *slot;
*slot = newValue;
} else {
// atomic需要加锁、
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
锁的分类
- 自旋锁 效率比较高,在等待过程中不会释放CPU资源,一直do-while。(有资料说,并不是死循环,也用到了休眠。待验证)
- 互斥锁 效率一般,等待过程中会释放CPU资源。
OSSpinLock
需要引入头文件。#import <libkern/OSAtomic.h>
由于该锁会出现优先级反转的bug,因为iOS10开始废弃了。
'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from <os/lock.h> instead
// 初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(lock);
// 尝试加锁,加锁失败,可以先处理其他的任务
if (OSSpinLockTry(lock)){
}
// 解锁
OSSpinLockUnlock(lock);
os_unfair_lock
有资料说是自旋锁,也有的说是互斥锁。官方的解释是为在内核中等待,不会一直占用CPU。这样比较像互斥锁。但是普通的互斥锁是在应用进程中等待,不是在内核中等待,此时又不太像互斥锁。
个人的理解偏好为自旋锁,因为如果占用CPU只是单纯了降低了CPU的利用率,但是通过观察者模式,如果好了,直接在内核中唤醒。其实本质是比自旋锁更节省CPU的锁。比自旋锁还牛逼的,还归纳为互斥锁,个人感觉它会有点不开心。核心就是互斥锁会让出CPU执行权限并等待CPU再次唤醒,而os_unfair_lock
没有完全让出CPU权限,对CPU说,我要干活,只是我的CPU周期你可以临时借用一下。
// 初始化锁
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(&lock);
// 尝试加锁
if (os_unfair_lock_trylock(&lock)){
}
NSLock
NSLock *lock = [[NSLock alloc] init];
lock.name = @"我是一把帅气的锁";
// 加锁
[lock lock];
// 解锁
[lock unlock];
// 尝试加锁
if ([lock tryLock]){
}
// 尝试加锁,防止一直等待,超过3秒则加锁失败。
if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:3]]){
}
NSCondition
lock
加锁unlock
解锁wait
阻塞当前线程,使线程休眠,等待唤醒。调用前必须已经加锁。waitUntilDate
超时了也会自动唤醒。signal
唤醒一个正在休眠的线程,如果需要唤醒多个,需要调用多次。如果没有线程等待,则什么也不做。调用前必须已经被加锁。broadcast
唤醒所有的线程。如果没有线程在等在,什么也不做。调用前必须已经加锁。name
修改名称
NSCondition *lock = [[NSCondition alloc] init];
__block int count = 0;
for (int i=0; i< 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
count ++;
[lock unlock];
});
}
for (int i=0; i< 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[lock lock];
count --;
NSLog(@"售卖一件商品,当前库存有%d",count);
[lock unlock];
});
}
线程唤醒
NSCondition *lock = [[NSCondition alloc] init];
__block int count = 0;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
NSLog(@"生产者begin");
[lock lock];
NSLog(@"生产者获取锁");
count ++;
[lock signal];// 唤醒之前的等待
[lock unlock];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
sleep(2);
NSLog(@"生产者begin");
[lock lock];
NSLog(@"生产者获取锁");
count ++;
[lock signal];// 唤醒之前的等待
[lock unlock];
});
});
for (int i=0; i< 3; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"消费者%d begin", i);
[lock lock];
NSLog(@"消费者%d获取锁", i);
while (count == 0){
NSLog(@"消费者%d等待生产者", i);
[lock wait];
}
count --;
NSLog(@"售卖一件商品,当前库存有%d",count);
[lock unlock];
});
}
NSConditionLock
该锁除了可以直接使用lock
和unlock
,还可以有条件的获取锁。
// 初始化锁
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1];
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
// 满足3的时候才会获取锁
[lock lockWhenCondition:3];
NSLog(@"3保存文件成功");
[lock unlockWithCondition:4];
});
dispatch_async(queue, ^{
[lock lockWhenCondition:2];
NSLog(@"2开始合并视频");
[lock unlockWithCondition:3];
});
dispatch_async(queue, ^{
[lock lockWhenCondition:1];
NSLog(@"1下载视频");
[lock unlockWithCondition:2];
});
NSRecursiveLock
递归锁。
该锁可以在一个线程中多次进行加锁。
- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (NSInteger)sum:(NSInteger )value{
[self.lock lock];
if (value == 1){
return 1;
}
NSInteger total = [self sum:value-1];
[self.lock unlock];
return value + total;
}
self.lock = [[NSRecursiveLock alloc] init];
NSLog(@"%d", [self sum:100]);
@synchronized
也可以在同一个线程中多次加锁。不同线程还是要等待的。
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
dispatch_async(q, ^{
@synchronized (self) {
NSLog(@"1");
sleep(2);
@synchronized (self) {
NSLog(@"2");
}
}
});
dispatch_async(q, ^{
@synchronized (self) {
NSLog(@"3");
sleep(3);
@synchronized (self) {
NSLog(@"4");
}
}
});
如果先打印1,则必然等等到2。才能等到3和4。
如果当前的线程中加锁,可以开启线程获取锁。
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
dispatch_async(q, ^{
@synchronized (self) {
NSLog(@"1");
sleep(2);
dispatch_async(q, ^{
@synchronized (self) {
NSLog(@"2");
}
});
}
});
读写锁
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
dispatch_barrier_async(q, ^{
// 写数据 因为多线程写会导致数据竞争
});
// 读取数据。其实这里用async和sync都可以。区别在于是否等待,都不会导致数据错误。主要是保证写的时候是正确的
dispatch_sync(q, ^{
});