iOS 多线程

174 阅读10分钟

线程的基本概念

线程和进程

进程是一个正在运行的程序,进程之间的内存空间是隔离的。

线程是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死亡状态:此时程序执行结束或者因为程序出错导致闪退。

image.png

同步和异步

同步:在当前线程阻塞执行,不会开启子线程。

线程面试题

题目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是第一个输出的。
其中26的顺序不确定,但是大部分是6先输出。因为开启异步线程和当前线程是独立的两个现场,没有顺序关系。
接着是245顺序输出。输出2之后,由于queue是并发队列,因此dispatch_sync会等待4输出完毕,再接着输出5.

题目2

输出162,然后死锁。
面试回答参考:输出16。因为创建的队列是同步队列,所以输出2之后,会等待任务3执行结束,而任务3必须等其它任务都执行结束才能开始。所以2一直等待3,3一直无法开始执行,从而导致死锁。同样,62是两个独立的线程,无法保证先后顺序,大部分是先62.

如果改成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

该锁除了可以直接使用lockunlock,还可以有条件的获取锁。

        // 初始化锁
        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。才能等到34

如果当前的线程中加锁,可以开启线程获取锁。

        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, ^{
            
        });