OC-多线程

作者: xiaoyouPrince | 来源:发表于2020-07-17 16:35 被阅读0次

    多线程

    官方文档:线程编程指南
    GCD源码:https://github.com/apple/swift-corelibs-libdispatch

    iOS 中常见的多线程方案

    iOS 中常将的多线程方案如下:


    iOS多线程方案

    GCD 多线程基本概念

    • 同步/异步: 是否有开启新线程的能力
    • 串行队列/并行队列: 是否具有并行执行任务的能力。主队列是一种特殊的串行队列
    队列概念

    Note:
    在主线程执行同步串行任务,会卡死主线程。
    原理: 在串行队列里面执行同步任务,就会产生死锁。

    多线程安全问题与解决

    多线程安全问题在于:多个线程同时访问并修改同一变量值,会造成最终值不正确。
    例如:存取钱问题、售票问题

    多线程同时修改资源导致异常

    解决方案: 使用线程同步技术(就是协同步调,按照预定的先后次序进行)。常见的线程同步技术为:加锁

    加锁同步多线程资源竞争

    原则: 对于修改同一个变量值,需要用同一个锁。如果只是读取,则无需加锁

    常用的锁(效率从高到底):

    • os_unfair_lock
    • OSSpinLock
    • dispatch_semaphore
    • pthread_mutex
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSCondition
    • pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized

    OSSpinLock

    OSSpinLock 自旋锁, 等待锁的线程会处于忙等状态(busy-wait),一直占用着CPU资源

    目前已经不再安全,可能会出现线程优先级翻转问题。表现上也类似死锁:如果等待锁的线程优先级较高,它就会一直占用CPU资源,优先级低的线程就无法释放锁

    已经在iOS10开始被废弃。需要引入头文件#import <libkern/OSAtomic.h>,使用如下:

    #import <libkern/OSAtomic.h>
    
    
    // 初始化锁
    OSSpinLock lock = OS_SPINLOCK_INIT;
    
    // 加锁
    OSSpinLockLock(&lock);
    
        // 中间需要做的操作...
        
    // 解锁
    OSSpinLockUnlock(&lock); 
    
    /////////////////////////////////////////////
    iOS 10之后替代 os_unfair_lock 头文件<os/lock.h>
    

    os_unfair_lock

    os_unfair_lock 作为 OSSpinLock 的替代品,解决了优先级反转问题,能做到让等待的线程处于真正的休眠状态,其接口与OSSpinLock 相似。需导入头文文件<os/lock.h>

    #import <os/lock.h> 
    
    // 初始化
    os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
    
    // 加锁/尝试加锁
    void os_unfair_lock_lock(os_unfair_lock_t lock);
    bool os_unfair_lock_trylock(os_unfair_lock_t lock);
    
    // 解锁
    void os_unfair_lock_unlock(os_unfair_lock_t lock);
    
    // 判断是否锁的拥有者是自己,
    void os_unfair_lock_assert_owner(os_unfair_lock_t lock);
    void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);
    

    pthread_mutex

    pthread_mutex 能做到让等待的线程处于休眠状态。需要引入头文件 <pthread.h>

    互斥锁/递归锁/条件锁

    // 普通互斥锁,属性传NULL
    pthread_mutex_init(&_mutex, NULL);
    pthread_mutex_lock(&_mutex);
        // 中间需要的操作
    pthread_mutex_unlock(&_mutex);
    
    ---递归锁 -----------------------
    // 递归锁:允许同一个线程对一把锁进行重复加锁
    // 初始化属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        // 初始化锁
        pthread_mutex_init(mutex, &attr);
        // 销毁属性
        pthread_mutexattr_destroy(&attr);
    
    pthread_mutex_lock(&_mutex);
        // 中间需要的操作
    pthread_mutex_unlock(&_mutex);
    
    ----条件锁--------------------------
    当多线程执行任务有条件依赖的是可以用条件锁。
    - (void)__remove
    {
        pthread_mutex_lock(&_mutex);
        
        if (self.data.count == 0) {
            // 等待
            pthread_cond_wait(&_cond, &_mutex);
        }
        
        [self.data removeLastObject];
        pthread_mutex_unlock(&_mutex);
    }
    
    // 线程2
    // 往数组中添加元素
    - (void)__add
    {
        pthread_mutex_lock(&_mutex);
        sleep(1);
        
        [self.data addObject:@"Test"];
        
        // 信号 - 唤醒被该条件加的锁
        pthread_cond_signal(&_cond);
        // 广播
    //    pthread_cond_broadcast(&_cond);
        pthread_mutex_unlock(&_mutex);
    }
    

    NSLock、NSRecursiveLock、NSCondition、NSConditionLock

    这几个锁是基于 pthread_mutex 的 OC 封装。其使用更加简单、更加面向对象。

    // NSLock - 封装自 pthread_mutex_lock 默认锁
    self.lock = [[NSLock alloc] init]; 
    [self.ticketLock lock];
        // 加锁代码
    [self.ticketLock unlock];
    
    // NSCondition -- 封装自 pthread_mutex_lock 默认条件锁
    self.condition = [[NSCondition alloc] init];
    [self.condition lock];
    // 等待
    [self.condition wait];
    // 信号
    [self.condition signal];    
    // 广播
    [self.condition broadcast];
    [self.condition unlock];
    
    // NSConditionLock -- 封装自 pthread_mutex_lock 条件锁,可加自定义条件
    self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
    // 以下三段代码可以按顺序执行
    [self.conditionLock lock];
    NSLog(@"__one");
    [self.conditionLock unlockWithCondition:2];
    
    [self.conditionLock lockWhenCondition:2];
    NSLog(@"__two");
    [self.conditionLock unlockWithCondition:3];
    
    [self.conditionLock lockWhenCondition:3];
    NSLog(@"__three");
    [self.conditionLock unlock];
    
    // 
    

    dispatch_queue(DISPATCH_QUEUE_SERIAL)

    使用串行队列也能解决多线程资源竞争问题,将线程加入到串行队列按顺序执行。

    self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(self.serialQueue, ^{
            // 处理变量赋值等核心功能代码
        });
    

    dispatch_semaphore

    semaphore 叫做“信号量”,用来控制线程的最大并发数量。

    如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码。
    如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码。

    dispatch_semaphore_signal(); 给对对应的信号量 +1

    semaphore 初始值为1时候,非常适合做线程同步

    // 设置最大允许并发数 5
    self.semaphore = dispatch_semaphore_create(5);
    
    - (void)test
    {
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        // 相关代码
        
        // 让信号量的值+1
        dispatch_semaphore_signal(self.semaphore);
    }
    

    @synchronized

    @synchronized 是对 mutex 递归锁的封装。可以参考 runtime 源码 objc_sync源码。

    // 参数即要设置为锁的值,就是一个指针
    @synchronized([self class]) {
            [super __drawMoney];
        }
    

    锁的使用小技巧: 宏

    #define SemaphoreBegin \
    static dispatch_semaphore_t semaphore; \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
        semaphore = dispatch_semaphore_create(1); \
    }); \
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    #define SemaphoreEnd \
    dispatch_semaphore_signal(semaphore);
    
    --------------
    SemaphoreBegin;
    // .....    
    SemaphoreEnd;
    

    atomic 原子操作

    写属性的时候常用 atomic、nonatomic

    给属性加上 atomic 修饰,可以保证属性 setter 和 getter 方法都是原子性操作,也就是保证 setter 和 getter 内部都是线程同步的。这里可以参考 runtime 源码 objc-accessors。本质上也是加锁,源码如下

    // 获取属性对象
    id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
        if (offset == 0) { // 对象本质为结构体,根据属性的在结构体内的 offset 获取。如果offset == 0 即获取结构体首地址,即 isa 地址
            return object_getClass(self);
        }
    
        // Retain release world
        // 根据 offset 获取结构体内 属性指针
        id *slot = (id*) ((char*)self + offset);
        // 如果是非原子属性,就直接返回属性指针
        if (!atomic) return *slot;
            
        // Atomic retain release world
        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);
    }
    

    atomic 给 setter/getter 内部加锁保证了属性存取的安全,但是不能保证属性取出之后的操作安全。

    因为存取方法使用过于频繁,所以 atomic 显得过于消耗性能。

    iOS 中的读写安全方案

    IO 操作 -> 文件读写操作 -> 【多度,单写】

    实际操作条件:

    1. 同一时间,只能有一个线程进行写操作
    2. 同一时间,允许有多个线程进行读操作
    3. 同一时间,不允许既有写操作,又有读操作

    方案如下:

    1. pthread_rwlock
    2. dispatch_barrier_sync

    pthread_rwlock

    pthread_rwlock 也是互斥锁,等待锁的进程会进入休眠。使用如下

    // 创建读写锁属性
    pthread_rwlockattr_t rwAttr;
    pthread_rwlockattr_init(&rwAttr);
        
    // 初始化锁
    pthread_rwlock_t lock;
    pthread_rwlock_init(&lock, &rwAttr); // pthread_rwlock_init(&lock, NULL);
    // 读-加锁
    pthread_rwlock_rdlock(&lock);
    // 读-尝试加锁
    pthread_rwlock_tryrdlock(&lock);
    // 写-加锁
    pthread_rwlock_wrlock(&lock);
    // 写-尝试加锁
    pthread_rwlock_trywrlock(&lock);
    // 解锁
    pthread_rwlock_unlock(&lock);
    // 销毁
    pthread_rwlock_destroy(&lock);
    

    dispatch_barrier_sync

    • 这个函数闯入的并发队列,必须是自己通过dispatch_queue_create创建的
    • 如果传入的是一个串行或者全局并发队列,那就相当于调用dispatch_async函数
    // 创建队列
    dispatch_queue_t  _Nonnull queue = dispatch_queue_create("barrierQueue", DISPATCH_QUEUE_CONCURRENT);
        
    // 读 - 异步线程,可以多线程同时访问
    dispatch_barrier_async(queue, ^{
        
    });
        
    // 写 - 同步任务,只有一个线程可以写
    dispatch_barrier_sync(queue, ^{
        
    });
    

    面试题

    • 你理解的多线程?
    线程是应用程序内部实现多个执行路径的相对轻量的方法。
    
    系统->并行执行进程->进程执行一个或者多个线程。 
    这些线程可以同时或者几乎同时的方式执行不同的任务。
    系统本身实际上管理这些执行的线程,安排它们在可用的内核上运行,并根据需要中断它们,将执行时间分配给其他线程。
    
    多线程有点:
    1. 可以提高程序的感知响应能力,
    2. 可以提高应用程序在多核系统上的实时性能
    
    缺点:
    1. 增加代码复杂性,它们可以访问同样的资源,多个线程需协同合作,防止破坏程序的状态信息
    2. 线程间的资源竞争问题,需要线程同步的技术来额外处理
    
    • 以下代码执行情况如何?正确执行/奔溃?why?
    - (void)interview01
    {
        // 会产生死锁!卡死主线程
        NSLog(@"执行任务1");
        
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
            NSLog(@"执行任务2");
        });
        
        NSLog(@"执行任务3");
        
        // dispatch_sync立马在当前线程同步执行任务
    }
    
    - (void)interview02
    {
        // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
        NSLog(@"执行任务1");
        
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_async(queue, ^{
            NSLog(@"执行任务2");
        });
        
        NSLog(@"执行任务3");
        
        // dispatch_async不要求立马在当前线程同步执行任务
    }
    
    - (void)interview03
    {
        // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
        NSLog(@"执行任务1");
        
        dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{ // 0
            NSLog(@"执行任务2");
            
            dispatch_sync(queue, ^{ // 1
                NSLog(@"执行任务3");
            });
        
            NSLog(@"执行任务4");
        });
        
        NSLog(@"执行任务5");
    }
    
    - (void)interview04
    {
        // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
        NSLog(@"执行任务1");
        
        dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    //    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
        
        dispatch_async(queue, ^{ // 0
            NSLog(@"执行任务2");
            
            dispatch_sync(queue2, ^{ // 1
                NSLog(@"执行任务3");
            });
            
            NSLog(@"执行任务4");
        });
        
        NSLog(@"执行任务5");
    }
    
    - (void)interview05
    {
        // 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
        NSLog(@"执行任务1");
        
        dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_async(queue, ^{ // 0
            NSLog(@"执行任务2");
            
            dispatch_sync(queue, ^{ // 1
                NSLog(@"执行任务3");
            });
            
            NSLog(@"执行任务4");
        });
        
        NSLog(@"执行任务5");
    }
    
    • 下面代码打印什么?为什么?
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self test2];
    }
    
    - (void)test
    {
        NSLog(@"2");
    }
    
    - (void)test2
    {
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_async(queue, ^{
            NSLog(@"1");
            [self performSelector:@selector(test) withObject:nil afterDelay:.0];
            NSLog(@"3");
        });
    }
    
    // 打印 1、3
    // 原因: performSelector:withObject:afterDelay 这个方法本质上是给Runloop 添加定时器。而子线程虽然已经创建了 runloop 但是并没有运行,所以不会打印,处理方式就是运行子线程的 runloop,让子线程保活
    
    // 以下代码同理
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSThread *thread = [[NSThread alloc] initWithBlock:^{
            NSLog(@"1");
    
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }];
        [thread start];
    
        [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
    }
    
    • 如何实现如首页多个网络请求,最后一个请求基于前面的网络请求的情况
    // 使用 dispatch_group 的方式。
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        // 创建队列组
        dispatch_group_t group = dispatch_group_create();
        // 创建并发队列
        dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
        
        // 添加异步任务
        dispatch_group_async(group, queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务1-%@", [NSThread currentThread]);
            }
        });
        
        dispatch_group_async(group, queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务2-%@", [NSThread currentThread]);
            }
        });
        
        // 等前面的任务执行完毕后,会自动执行这个任务
    //    dispatch_group_notify(group, queue, ^{
    //        dispatch_async(dispatch_get_main_queue(), ^{
    //            for (int i = 0; i < 5; i++) {
    //                NSLog(@"任务3-%@", [NSThread currentThread]);
    //            }
    //        });
    //    });
        
    //    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //        for (int i = 0; i < 5; i++) {
    //            NSLog(@"任务3-%@", [NSThread currentThread]);
    //        }
    //    });
        
        dispatch_group_notify(group, queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务3-%@", [NSThread currentThread]);
            }
        });
        
        dispatch_group_notify(group, queue, ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务4-%@", [NSThread currentThread]);
            }
        });  
    }
    
    • iOS 的多线程有几种方案,你更倾向于哪一种?
    pthread
    NSThread
    GCD  ---> 更倾向
    NSOperation
    
    • 你在项目中用过 GCD 吗?
    用过
    如: 
    dispatch_semaphore -> 信号量
    dispatch_barrier
    dispatch_queue
    dispatch_group
    dispatch_sync & dispatch_async
    ...
    
    • GCD 的队列类型
    串行队列 & 并行队列
    
    • 说一下 OperationQueue 和 GCD 的区别,以及各自优势?
    GCD:
        基于C语言的API,旨在替代 NSThread 的线程技术,可以高效利用设备多核。
    
    OperationQueue:
        底层封装自 GCD,增加了很多使用功能,更加面向对象。
    
    • 线程安全处理的手段有哪些?
    1. 加锁
    2. 使用 GCD 串行队列
    3. 使用 GCD 信号量
    
    • OC 你了解的锁有哪些?在你的回答基础上进行二次提问
      • 1.自旋锁和互斥锁的对比
      • 2.使用以上锁需要注意哪些?
      • 3.使用 C/OC/C++,任选其一,实现自旋或互斥?口述即可
    了解的锁:
    OSSpinLock、os_unfair_lock、pthread_mutex、NSLock、NSCondition、NSRecursiveLock、NSConditionLock、@synchronized
    
    自旋锁适合的场景
    1. 预计线程等待锁的时间很短
    2. 加锁的代码(临界区)经常被调用,但竞争情况不是很激烈
    3. CPU 资源不是很紧张
    4. 多核处理器
    
    互斥锁比较适合的场景
    1. 预计线程等待的时间较长
    2. 单核处理器(减少CPU占用)
    3. 临界区有 IO 操作(IO 操作本身占CPU)
    4. 临界区代码复杂或者循环量很大
    5. 临界区竞争激烈
    

    相关文章

      网友评论

        本文标题:OC-多线程

        本文链接:https://www.haomeiwen.com/subject/chaghktx.html