iOS-面试题3-多线程

目录:

  1. GCD
  2. 加锁方案

一. GCD

  1. 说一下iOS中多线程的实现方案
多线程方案.png

① 这些多线程方案的底层都是依赖pthread
② NSThread线程生命周期是程序员管理,GCD和NSOperation是系统自动管理
③ NSThread和NSOperation都是OC的,更加面向对象
④ NSOperation基于CGD,使用更加面向对象

  1. 同步、异步、串行、并发的区别
    同步(sync):在当前线程中执行任务,不具备开启新线程的能力
    异步(async):在新的线程中执行任务,具备开启新线程的能力
    串行:一个任务执行完毕后,再执行下一个任务
    并发:多个任务并发(同时)执行

  2. dispatch_sync和dispatch_async的区别
    dispatch_sync同步的方式执行任务,不具备开启线程的能力,它的任务在当前线程执行的。
    dispatch_async异步的方式执行任务,具备开启线程的能力,它的任务在子线程执行的。

  3. GCD的队列分类
    ① 串行队列(Serial Dispatch Queue)
    让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
    ② 并发队列(Concurrent Dispatch Queue)
    可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    并发功能只有在异步(dispatch_async)函数下才有效

dispatch_sync和dispatch_async函数决定是否在当前线程执行,串行队列和并发队列决定任务是串行执行还是并发执行,他们决定的东西互不影响。

  1. GCD函数和队列的组合
全局并发队列 手动创建的串行队列 主队列
同步(sync) 没有开启新线程
串行执行任务
没有开启新线程
串行执行任务
死锁
异步(async) 有开启新线程
并发执行任务
有开启新线程
串行执行任务
没有开启新线程
串行执行任务
  1. 死锁产生的原因?
    首先要明白两个特点:
    队列的特点:FIFO (First In First Out),先进先出
    dispatch_sync函数的特点:sync函数要求立马在当前线程同步执行任务
    原因:使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)

  2. performSelector:withObject:和performSelector:withObject:afterDelay:的区别?
    ① performSelector:withObject:的内部直接就是objc_msgSend。
    ② performSelector:withObject:afterDelay:的内部会创建一个RunLoop,再创建一个定时器,再把定时器添加到RunLoop里面,但是并没有运行RunLoop。
    ③ run方法内部的确使用while循环一直在调用runMode:beforeDate:。

  3. iOS常见的延时执行的方式
    ① 调用NSObject的方法:performSelector:withObject:afterDelay:,该方法在哪个线程调用,那么run就在哪个线程执行,通常是主线程。
    ② 使用dispatch_after函数,传入一个队列,如果是主队列,那么就在主线程执行,如果是并发队列,那么会新开启一个线程,在子线程中执行。

  4. 如何用GCD实现,异步并发执行任务1、任务2,等任务1、任务2都执行完毕后,再回到主线程执行任务3?
    使用GCD队列组,dispatch_group_notify(group, queue, ^{}) 函数会在group里面的任务执行完再执行,如果queue是主队列就在主线程执行这个函数的任务,如果queue是全局并发队列,就在子线程异步执行这个函数的任务。

博客地址:GCD

二. 加锁方案

介绍一下iOS中的加锁方案

OSSpinLock 自旋锁
os_unfair_lock
pthread_mutex
dispatch_semaphore 信号量
dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
NSLock
NSRecursiveLock
NSCondition
NSConditionLock
@synchronized

1. OSSpinLock

  • OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。
  • 目前已经不再安全,可能会出现优先级反转问题,也就是,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
  • 自旋锁就是下面方式②,类似于写了个while循环,一直占用cpu资源。
  • 线程阻塞方式有:
    ① 让线程睡眠,不占用cpu资源
    ② while循环,一直占用cpu资源

2. os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持,从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁。所以如果忘记解锁了,那么当下一个线程进来的时候发现没有解锁,就会休眠,当所有的线程都进来 ,就会导致所有的线程都会休眠,这种情况称为死锁,就是永远拿不到锁。

3. pthread_mutex

  • mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
  • pthread_mutex是跨平台的,一般pthread开头的都是跨平台的,iOS、Linux、Windows等都能使用

pthread_mutexattr_settype有三种类型,一般我们只用默认的锁、递归锁,如下:

#define PTHREAD_MUTEX_NORMAL        0 默认的锁
#define PTHREAD_MUTEX_ERRORCHECK    1 检查错误的锁
#define PTHREAD_MUTEX_RECURSIVE     2 递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL 默认的锁
  1. 递归调用产生死锁,用递归锁解决
    递归锁:允许同一个线程对一把锁进行重复加锁(解锁)

  2. 自旋锁和互斥锁的区别:
    自旋锁的本质是while循环。
    互斥锁是通过线程休眠实现的。
    这里讲的锁,除了OSSpinLock是自旋锁,其他的都是互斥锁。

  3. pthread_mutex – 条件锁的用法

假设需求是,我们有两个线程,一条线程是删除东西,一条线程是添加东西,删东西和添加东西都要加锁,删除东西时候有个条件,就是必须要有东西才进行删除,那么如何实现?

// 初始化条件
pthread_cond_init(&_cond, NULL);
...
//如果数组为0,就先等等,等有东西了再删除
if (self.data.count == 0) {
    // 等待
    pthread_cond_wait(&_cond, &_mutex);
}
...
// 信号 唤醒上面那个线程
pthread_cond_signal(&_cond);

应用场景:
线程1依赖线程2,需要线程2做完某件事再回到线程1继续做事情。比如厂家和消费者的关系,消费者要想买东西,必须要厂家先生产东西。

4. NSLock、NSRecursiveLock、NSCondition

NSLock是对mutex普通锁的封装
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装

5. NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。
使用场景:
如果子线程有依赖关系(子线程的执行是有顺序的),就可以使用NSConditionLock,设置条件具体的值。

6. dispatch_queue(DISPATCH_QUEUE_SERIAL)

直接使用GCD的串行队列,也是可以实现线程同步的

dispatch_queue_t moneyQueue = dispatch_queue_create("moneyQueue", DISPATCH_QUEUE_SERIAL);
···
- (void)__drawMoney {
    dispatch_sync(moneyQueue, ^{
        [super __drawMoney];
    });
}
- (void)__saveMoney {
    dispatch_sync(moneyQueue, ^{
        [super __saveMoney];
    });
}

dispatch_sync函数的特点:要求立马在当前线程同步执行任务(当前线程是子线程,在MJBaseDemo里面已经写了)。

举例说明:比如线程4进来卖票,那么这个操作就会被放到串行队列中,等一会线程7又进来卖票,这个操作也会被放到串行队列中,串行队列里面的东西是:线程4的卖票操作 - 线程7的卖票操作 - 线程5的卖票操作。这样线程4卖完,线程7卖,线程7卖完,线程5卖。这样串行队列中的任务是异步的,不会出现多个线程同时访问一个成员变量的问题,这样也能解决线程安全问题。所以说,线程同步问题也不是必须要通过加锁才能实现。

7. dispatch_semaphore

semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_semaphore_t ticketSemaphore = dispatch_semaphore_create(1);
//如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
//如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
//第二个参数代表等到啥时候,传入的DISPATCH_TIME_FOREVER,代表一直等
dispatch_semaphore_wait(ticketSemaphore, DISPATCH_TIME_FOREVER);
......
//让信号量的值+1
dispatch_semaphore_signal(ticketSemaphore);

小提示:控制线程并发访问的最大数量也可以用:

NSOperationQueue *queue;
queue.maxConcurrentOperationCount = 5;

8. @synchronized

@synchronized是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

- (void)__drawMoney {
    //最简单的一种方式,但是性能比较差,苹果不推荐使用,所以打出来的时候没提示。
    //其中()中是拿什么当做一把锁,比如下面是拿当前类对象当做一把锁。
    //为什么把类对象当做一把锁?因为类对象只有一个,以后无论什么实例对象调用这个方法,都是类对象作为锁,这样就只有一把锁,才能锁住。
    @synchronized([self class]) {
        [super __drawMoney];
    }
}
- (void)__saveMoney {
    @synchronized([self class]) { // objc_sync_enter
        [super __saveMoney];
    } // objc_sync_exit
}

注意:@synchronized和@synthesize、@dynamic不一样,别弄混淆了,关于@synthesize、@dynamic可参考Runtime3-objc_msgSend底层调用流程的补充内容。

9. 各种锁的总结

OSSpinLock 自旋锁,因为底层是使用while循环进行忙等,不会进行休眠和唤醒,所以是性能比较高的一把锁,但是现在已经不安全,被抛弃。

os_unfair_lock 用于取代不安全的OSSpinLock ,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等,是一种互斥锁。

pthread_mutex mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。
它是跨平台的,当传入的类型是默认的就是默认锁,当传入PTHREAD_MUTEX_RECURSIVE,就是递归锁,还可以通过pthread_cond_wait(&_cond, &_mutex)当做条件锁来使用。

dispatch_semaphore 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_queue(DISPATCH_QUEUE_SERIAL) 直接使用GCD的串行队列,也是可以实现线程同步的

NSLock是对mutex普通锁的封装
NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition是对mutex和cond的封装
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

@synchronized也是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

10. 各种锁性能比较

性能从高到低排序:

os_unfair_lock iOS10开始支持
OSSpinLock 不安全,被抛弃
dispatch_semaphore 如果需要iOS8、9都支持可以使用
pthread_mutex 可以跨平台
dispatch_queue(DISPATCH_QUEUE_SERIAL) 本来GCD效率就很高
NSLock 对mutex普通锁的封装
NSCondition 对mutex和cond的封装
pthread_mutex(recursive) mutex递归锁,递归锁效率本来就低
NSRecursiveLock 对mutex递归锁的封装
NSConditionLock 对NSCondition的封装
@synchronized 对mutex递归锁的封装

一般推荐使用os_unfair_lockdispatch_semaphorepthread_mutex

  1. 说一下 OperationQueue 和 GCD 的区别,以及各自的优势有哪些?

  2. 什么情况使用自旋锁比较划算?
    预计线程等待锁的时间很短
    加锁的代码(临界区)经常被调用,但竞争情况很少发生
    CPU资源不紧张
    多核处理器

  3. 什么情况使用互斥锁比较划算?
    预计线程等待锁的时间较长
    单核处理器
    临界区有IO操作,因为IO操作比较占用CPU资源
    临界区代码复杂或者循环量大
    临界区竞争非常激烈

  4. nonatomic和atomic
    对于atomic,setter、getter方法内部会有加锁、解锁操作,nonatomic没有。

  5. 既然atomic是线程安全的,那么为什么开发中我们基本不用呢?
    ① 太耗性能了,因为setter、getter方法调用次数太频繁了,如果每次都需要加锁、解锁,那手机CPU资源不就被你消耗完了。所以atomic一般在MAC上才使用。
    ② 而且只有多条线程同时访问同一个对象的属性,才会有线程安全问题。这种情况几乎没有,如果你非要造出来这种情况,比如:多条线程同时访问 p.data ,那你完全可以在外面加锁嘛!

  6. iOS多读单写实现方案
    显然,如果使用上面讲的加锁方案,那么无论读、写,同一时间只有一条线程在执行,这样效率比较低,实际上读操作可以同时多条线程一起执行的。iOS中多读单写的实现方案有:
    ① pthread_rwlock_t:读写锁
    ② dispatch_barrier_async:异步栅栏调用

- (void)read {
    //读-加锁
    pthread_rwlock_rdlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    //解锁
    pthread_rwlock_unlock(&_lock);
}
- (void)write {
    //写-加锁
    pthread_rwlock_wrlock(&_lock);
    sleep(1);
    NSLog(@"%s", __func__);
    //解锁
    pthread_rwlock_unlock(&_lock);
}
//读
dispatch_async(self.queue, ^{
    [self read];
});
//写
//当有一条线程在执行这个任务的时候,绝不允许queue中有其他线程在执行其他任务(包括上面的read和下面的write)
dispatch_barrier_async(self.queue, ^{
    [self write];
});

//注意:这个dispatch_barrier_async函数传入的并发队列必须是自己手动通过dispatch_queue_cretate创建的。
//如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果。

博客地址:
线程安全、OSSpinLock
加锁方案1
加锁方案2

你可能感兴趣的:(iOS-面试题3-多线程)