回答-阿里、字节:一套高效的iOS面试题⑥(多线程)

494 阅读9分钟

1、iOS开发中有多少类型的线程?分别对比

Pthread : 跨平台的C语言多线程框架,使用难度大。

NSThread : 面向对象,需手动管理生命周期。

GCD:Grand Central Dispatch ,旨在替代NSThread,主要是任务配合队列使用。

NSOperation:是对GCD的封装,更加面向对象。

2、GCD有哪些队列,默认提供哪些队列

1、主队列 (dispatch_get_main_queue())
2、全局并发队列 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

可以设置队列的优先级,从高到底为:

DISPATCH_QUEUE_PRIORITY_HIGH

DISPATCH_QUEUE_PRIORITY_DEFAULT

DISPATCH_QUEUE_PRIORITY_LOW

DISPATCH_QUEUE_PRIORITY_BACKGROUND

3、自定义队列(串行 DISPATCH_QUEUE_SERIAL 与并行 DISPATCH_QUEUE_CONCURRENT)

dispatch_queue_create("这里是队列名字", DISPATCH_QUEUE_SERIAL)

3、GCD有哪些方法api

3.1、dispatch_queue_create

dispatch_queue_create.png

能够创建串行队列、并发队列、全局队列。

1、申请内存空间

2、设置基本属性,如果是并发队列设置并发数是UINT32_MAX、如果是串行队列并发数设置为1

3、向队列提交的任务,会被放到它的目标队列来执行。

3.2、dispatch_async

dispatch_async.png

3.2.1、dispatch_async第一阶段的工作:主要是封装外部任务并添加到队列的链表中

3.2.2、队列唤醒的逻辑:主队列和全局队列的唤醒和任务执行逻辑

如果是主队列:

会先唤醒队列,然后唤醒主线程的Runloop。当我们调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。

如果是全局队列:

会去检测当前线程池是否可用(已满),未满创建新的线程。然后在创建新的线程中执行_dispatch_worker_thread函数。

dispatch_async如何实现?分发到主队列与全局队列有什么区别,一定会新建线程执行任务么?

1、dispatch_async 会把任务添加到队列链表中,添加完成后唤醒队列。

2、如果分发到全局队列唤醒时会从线程池中取出可用线程,如果没有会创建新线程,然后在线程中执行队列取出的任务。

3、如果分发到主队列会唤醒主线程的runloop,然后在runloop中通知GCD执行主队列提交的任务。

3.3、dispatch_sync

dispatch_sync.png

1、调用dispatch_sync时,会判断队列当前的状态,如果队列无正在执行的任务,直接执行任务

2、如果队列存在正在执行的任务或者处于挂起状态,需要等待信号量也唤醒

3、队列中正在执行的任务执行完之后,信号量唤醒队列,开始执行队列中的下一个任务

dispatch_sync 如何实现?

dispatch_sync一般在当前线程执行,如果是主队列的任务还会切换到主线程执行。

dispatch_sync的使用与线程绑定的信号量来实现串行执行的功能。

3.4、dispatch_barrier_async

dispatch_barrier_async.png

1、在全局队列中执行,效果同dispatch_async

2、在串行队列执行,效果同dispatch_sync

3、在自定义的并行队列中执行,执行队列的invoke函数,在while 循环中遍历取出任务,如果执行Barrier Block

需要等待前面的任务执行完---> 修改suspend_count (暂停标志位)保证其他任务不会同步执行 -----> Barrier Block 执行完之后重置suspend_count(暂停标志位),并执行后面的任务。

4、GCD主线程 & 主队列的关系

如果任务分发到主队列,会唤醒主线程的runloop,然后在runloop中通知GCD执行主队列提交的任务。

5、如何实现同步,有多少方式就说多少

1、dispatch_async(在同一个串行队列, ...)

2、dispatch_sync

3、NSOperationQueue.maxConcurrentOperationCount = 1

4、各种锁(OSSpinLock、os_unfair_lock、pthread_mutex、NSLock、NSRecursiveLock、NSConditionLock)

5、@synchronied(obj)

6、信号量 dispatch_semaphore_create() + dispatch_semaphore_wait()

6、dispatch_once实现原理

dispatch_once能保证任务只会被执行一次,即使同时多线程调用也是线程安全的。

利用原子性修改dispatch_token 标志位的值,信号量保证只有一个线程执行block,等待block执行完之后再唤醒所有等待中的线程。
void dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func) {
    struct _dispatch_once_waiter_s * volatile *vval =
            (struct _dispatch_once_waiter_s**)val;
    struct _dispatch_once_waiter_s dow = { NULL, 0 };
    struct _dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_t sema;

    if (dispatch_atomic_cmpxchg(vval, NULL, &dow, acquire)) {
        _dispatch_client_callout(ctxt, func);

        dispatch_atomic_maximally_synchronizing_barrier();
        // above assumed to contain release barrier
        tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE, relaxed);
        tail = &dow;
        while (tail != tmp) {
            while (!tmp->dow_next) {
                dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct _dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    } else {
        dow.dow_sema = _dispatch_get_thread_semaphore();
        tmp = *vval;
        for (;;) {
            if (tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            if (dispatch_atomic_cmpxchgvw(vval, tmp, &dow, &tmp, release)) {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
                break;
            }
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}
  1. 只有一个线程调用:此时传入的onceToken为空指针,if判断成立。通过_dispatch_client_callout执行block,然后将vval的值设为DISPATCH_ONCE_DONE,表示任务已完成,同时调用tmp保存先前的vval。此时,dow也为空,while判断不成立,代码执行结束。
  2. 同一线程第二次调用,此时vval已经变为了DISPATCH_ONCE_DONE,因此 if 判断不成立,进入 else 分支的 for 循环。由于 tmp 就是DISPATCH_ONCE_DONE,所以循环退出,没有做任何事。
  3. 多个线程同时调用,由于 if 判断中是一个原子性操作,所以必然只有一个线程能进入 if 分支,其他的进入 else 分支。由于其他线程在调用函数时,vval 还不是 DISPATCH_ONCE_DONE, 进入到for循环后半部分,这里构造了一个链表,链表的每个节点上都调用了信号量的 wait 方法并阻塞,而在 if 分支中,则会依次遍历所有的节点并调用 signal 方法,唤醒所有等待中的信号量。

7、什么情况下会死锁

1、对正在执行任务的串行队列添加同步任务。
/// 在主线程中执行这句代码
dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"这里死锁了");
});


/// 在哪里执行都可以
dispatch_queue_t theSerialQueue = dispatch_queue_create("我是个串行队列", DISPATCH_QUEUE_SERIAL);
dispatch_async(theSerialQueue, ^{ /// 正在执行任务,同步异步无所谓
    NSLog(@"第一层");
    
    /// 同一个串行队列
    dispatch_sync(theSerialQueue, ^{
        NSLog(@"这里死锁了");
        
    });
});
2、信号量死锁。
static dispatch_semaphore_t semaphore;

- (void)viewDidLoad {
    [super viewDidLoad];
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        semaphore = dispatch_semaphore_create(1);
    });
}

- (void)case3 {
        for (int i = 0; i<3; i++) {
            //会死锁
            [self semaphoreFunc];
        }
}

- (void)semaphoreFunc {
    NSLog(@"currentThread-%@",[NSThread currentThread]);
    //如果信号量的值 > 0 ,就让信号量的值减1,然后继续往下执行代码
    //如果信号量的值 <=0, 就会休眠等待,直到信号量的值变成>0 ,就让信号量的值减1,然后继续往下执行代码
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"---in");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"---ipad unlock");
        dispatch_semaphore_signal(semaphore);
    });
}
打印:
2022-02-10 11:26:59.025694+0800 OffcnApp[59181:7205444] currentThread-<NSThread: 0x600002a81dc0>{number = 1, name = main}
2022-02-10 11:26:59.025889+0800 OffcnApp[59181:7205444] ---in
2022-02-10 11:26:59.026021+0800 OffcnApp[59181:7205444] currentThread-<NSThread: 0x600002a81dc0>{number = 1, name = main}
原因分析:

1、第1次for循环执行执行完之后,semaphore的值会变成0。

2、dispatch_after这个操作是异步的,会把计时器的任务追加到主队列中。

3、第2次for循环时,semaphore = 0,线程会阻塞。

4、dispatch_after等待线程任务执行完之后再执行,而第2次for循环的任务需要等待dispatch_after释放信号量,造成了相互等的情况,导致了死锁。

在Objective-C中,多线程中的同步和异步有什么区别?

调用dispatch_sync意味着当前线程需要等待dispatch_sync中的任务完成之后再继续,而dispatch_async意味着当前线程不需要等待dispatch_async中任务完成直接继续往下执行。

把一些慢的耗时的操作异步处理,放在后台线程执行,目的是让主线程能够响应UI操作,而不是卡死。

reference:

stackoverflow.com/questions/2…

8、有哪些类型的线程锁,分别介绍下作用和使用场景

1、os_unfair_lock 等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
2、NSLock是对mutex普通锁的封装,NSLock 遵循 NSLocking 协议。

Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

3、NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

1、initWithCondition:初始化Condition,并且设置状态值

2、lockWhenCondition:(NSInteger)condition:当状态值为condition的时候加锁

3、unlockWithCondition:(NSInteger)condition当状态值为condition的时候解锁

4、dispatch_semaphore
  • semaphore叫做”信号量”
  • 信号量的初始值,可以用来控制线程并发访问的最大数量
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
5、dispatch_queue,使用GCD的串行队列也可以实现线程同步的
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 追加任务1
for (int i = 0; i < 2; ++i) {
NSLog(@"1---%@",[NSThread currentThread]);
}
});

dispatch_sync(queue, ^{
// 追加任务2
for (int i = 0; i < 2; ++i) {
NSLog(@"2---%@",[NSThread currentThread]);
}
});
6、@synchronized是对mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
7、atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,比如下面就不是线程安全的
@property (atomic,   strong) NSMutableArray *data; //加上atomic,就是对属性的getter、setter方法进行原子性操作,就是会对getter、setter方法加锁

p.data = [NSMutableArray array]; //这个设置过程是线程安全的
[[p data] addObject:@"1"]; //这个获取的过程是线程安全的,但是这个添加对象可能存在多个线程同时访问,就不是线程安全的
8、pthread_rwlock:读写锁,pthread_rwlock经常用于文件等数据的读写操作

iOS中的读写安全方案需要注意一下场景

  • 1、同一时间,只能有1个线程进行写的操作
  • 2、同一时间,允许有多个线程进行读的操作
  • 3、同一时间,不允许既有写的操作,又有读的操作
9、dispatch_barrier_async
//初始化
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
//读操作
dispatch_async(self.queue, ^{
});
//写操作
dispatch_barrier_async(self.queue, ^{
 
});
reference:

juejin.cn/post/684490…

9、NSOperationQueue中的maxConcurrentOperationCount默认值

如果对NSOperationQueue设置了maxConcurrentOperationCount值,它将根据可用处理器的数量和其他相关因素选择适当的值。

如果没有显式设置maxConcurrentOperationCount将会使用默认值,默认最大的操作数是根据当前系统的条件动态决定的。

最大的并发数并不会真正的为-1,-1 只是一个标识用来去判断有没有必要去动态获取合适的并发数。

如果设置的是0或者大于0的数,它是静态的,-1 是动态获取的。

reference:

stackoverflow.com/questions/1…

10、NSTimer、CADisplayLink、dispatch_source_t 的优劣

1、NSTimer,使用简单,运行依赖Runloop没有Runloop无法使用,一次Runloop循环耗时长的时候会导致不精确
具体使用:
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer invalidate];
2、CADisplaylink,依赖屏幕刷新频率触发事件,最精确适合做UI刷新,屏幕刷新频率受影响时时间也会受影响,它的取值的精度只能是一次屏幕刷新的最小间隔,比如60HZ的屏幕,间隔为16.67ms
具体使用:
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(takeTimer:)];
[link addToRunLoop:[NSRunLodop currentRunLoop] forMode:NSRunLoopCommonModes];
link.paused = !link.paused;
[link invalidate];
3、dispatch_source_t,不依赖runloop,获取的时间不精确而且使用麻烦
__block int countDown = 6;

/// 创建 计时器类型 的 Dispatch Source
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
/// 配置这个timer
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
/// 设置 timer 的事件处理
dispatch_source_set_event_handler(timer, ^{
    //定时器触发时执行
    if (countDown <= 0) {
        dispatch_source_cancel(timer);
        
        NSLog(@"倒计时 结束 ~~~");
    }
    else {
        NSLog(@"倒计时还剩 %d 秒...", countDown);
    }
    
    countDown--;
});

/// 启动 timer
dispatch_active(timer);
reference:

www.yuanchengwen.cn/2020/05/07/…

www.yuanchengwen.cn/2020/05/05/…

juejin.cn/post/684490…