多线程相关面试问题

1.0 GCD

        同步/异步 和 串行/并发

        dispatch_barrier_async 栅栏函数,解决多读单写的问题

        dispatch_group 

问题1:同步串行问题,以下片段会造成什么问题?如何解决?

- (void)viewDidLoad {
    dispatch_sync(dispatch_get_main_queue(), ^{
        [self doSomeThing];
    });
}

        会造成死锁。队列引起的循环等待

        完整回答:主队列提交了viewDidLoad一个任务一个blcok任务,无论哪个任务,最终都要分派到主线程 去处理,比如分派viewDidLoad到主线程中去处理,在它执行过程当中他需要调用block,当block同步调用完成之后,这个viewDidLoad才能继续向下,所以viewDidLoad任务需要依赖于后续提交的block任务,block要想执行,要先遵循队列的先进先出的性质,所以block依赖于viewdidload的完成 产生了相互等待


问题2:同步串行问题 以下片段会造成什么问题?如何解决?

- (void)viewDidLoad {
    dispatch_sync(serialQueue, ^{
        [self doSomeThing];
    });
}

        没问题 


        完整回答:viewDidLoad任务执行到某一刻的时候需要同步提交一个任务到对应的串行队列,由于同步方式提交的任务,同步提交在当前线程执行,串行队列所提交的block任务也是在主线程中执行,串行队列所提交的任务在主线程中执行完成后,在去主队列viewDidLoad方法后续的方法执行

如果没在viewDidLoad执行就会产生死锁和第一个问题一样的。同步分派到主队列会产生队列引起的循环等待死锁问题。


问题3:同步并发问题

         完整回答:只要是同步方式提交任务,无论提交串行队列还是并发队列,都是在当前线程去执行。所以打印12345 如果把上图的并发队列改为自定义的串行队列那么就产生死锁问题。


问题4:异步串行问题

        完整回答: 通过异步方式分派到全局并发队列之后,这个block会在GCD底层所维护的线程池中某一个线程去进行执行处理,而关于GCD底层所分派的线程默认情况下是没有开启对应RunLoop的,performSelector有效执行必须是它方法调用所属当前线程的有RunLoop的,没有的话就会失效。


2.0 dispatch_barrier_async()

        问题1:怎样利用GCD实现多读单写?让你实现一个多读单写的模型如何实现?

        


        完整答案:代码实现片段

@interface UserCenter()
{
    // 定义一个并发队列
    dispatch_queue_t concurrent_queue;
    
    // 用户数据中心, 可能多个线程需要数据访问
    NSMutableDictionary *userCenterDic;
}

@end

// 多读单写模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

//读取操作
- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步读取指定数据
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    
    return obj;
}

//写入操作
- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 异步栅栏调用设置数据
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}
@end

 3.0 dispatch_group

        问题1:使用GCD实现这个需求,A,B,C 三个任务并发任务,完成后执行任务D

        完整代码:代码实现片段

@interface GroupObject()
{
    dispatch_queue_t concurrent_queue;
    NSMutableArray <NSURL *> *arrayURLs;
}

@end

@implementation GroupObject

- (id)init
{
    self = [super init];
    if (self) {
        // 创建并发队列
        concurrent_queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        arrayURLs = [NSMutableArray array];
    }

    return self;
}

- (void)handle
{
    // 创建一个group
    dispatch_group_t group = dispatch_group_create();
    
    // for循环遍历各个元素执行操作
    for (NSURL *url in arrayURLs) {
        
        // 异步组分派到并发队列当中
        dispatch_group_async(group, concurrent_queue, ^{
            
            //根据url去下载图片
            
            NSLog(@"url is %@", url);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 当添加到组中的所有任务执行完成之后会调用该Block
        NSLog(@"所有图片已全部下载完成");
    });
}
@end

4.0 NSOperation

        需要和NSOperationQueue配合使用来实现多线程方案

        添加任务依赖

        任务执行状态控制

        最大并发量

任务执行状态控制

        isReady 当前任务是否处于就绪状态

        isExecuting 当前任务是否处于正在执行中状态

        isFinished 当前任务是否已经执行完成

        isCancelled 当前任务是否已取消

状态控制

        1:如果只重写main方法,底层控制变更任务执行完成状态,以及任务退出。

       2:如果重写了start方法,自行控制任务状态。底层实现-创建一个自定释放池,获取线程的优先级,状态的异常判断,如果都通过的话会判断当前状态是否处于执行中,如果没有执行的话去设置执行中的状态,会判断当前任务是否有被取消,如果没有被取消调用 [self main]函数,之后调用[self _finish]方法(kvo的方式变更finish的标记位系统会在合适的时机通过NSOperationQueue来监听kvo键值对,在合适的时机把NSOperation移除调),最后自动释放池的release的操作。

        问题1:系统是怎样移除一个isFinished=YES的NSOperation的?

        答案:通过kvo


5.0 NSThread

问题1:Start方法实现内部原理?

        首先做异常判断处理,之后调用pthread_create函数创建线程同时指定这个线程的启动函数,获取启动函数t,发送通知通知观察者当前线程已经启动,设置线程名称,调用线程main函数,最后调用关闭函数来关闭。

        如果想做一个长驻线程的效果,需要在main函数中做一个RunLoop的循环,main函数当中先做了一个异常判断,之后会通过调用target的selector来执行创建NSThread所指定的选择器,可以通过main函数里面调用的target的selector当中去维护这个事件循环,来达到常驻线程的目的


6.0 有哪些锁

问题1:iOS当中都有哪些锁?

        @synchronized  一般在创建单例对象的时候使用(保证在多线程环境下创建对象是唯一的)

        atomic  修饰属性的关键字,对被修饰对象进行原子操作(不负责使用)

        OSSpinLock(自旋锁)  循环等待访问,不释放当前资源。用于轻量级数据访问,简单的int值+1/-1操作 

        NSLock  一般是用于解决细力度的线程同步问题,来保证各个线程互斥进入自己的临界区。

        NSrecursiveLock(递归锁) 特点是可以重入

        dispatch_semaphore_t(信号量)

自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的
互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成
读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现

        


多线程面试问题总结

问题1:怎样用GCD实现多读单写?

@interface UserCenter()
{
    // 定义一个并发队列
    dispatch_queue_t concurrent_queue;
    
    // 用户数据中心, 可能多个线程需要数据访问
    NSMutableDictionary *userCenterDic;
}

@end

// 多读单写模型
@implementation UserCenter

- (id)init
{
    self = [super init];
    if (self) {
        // 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
        concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
        // 创建数据容器
        userCenterDic = [NSMutableDictionary dictionary];
    }
    
    return self;
}

//读取操作
- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步读取指定数据
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    
    return obj;
}

//写入操作
- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 异步栅栏调用设置数据
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}
@end

问题2: iOS系统为我们提供了几种多线程技术各自的特点是怎样的?

        GCD,NSOperation,NSThread

        一般使用GCD实现一些简单的线程同步,子线程的分派,实现类似于多读单写场景的解决。

        NSOperation 比如说第三方框架AFN,SDWebImageView,他里面都会涉及NSOperation,由于它的特点可以方便对任务的状态进行控制,包括可以控制添加依赖移除依赖。

        NSThread 一般用于实现常驻线程。

        他们的特点就要结合实际的应用场景采取对应的方案。

问题3:NSOperation对象在Finished之后是怎样从队列当中移除掉的?

        会通过kvo的方式通知他所对应的NSOperationQueue达到对对象移除的目的。

问题4:你都用过哪些锁?结合实际谈谈你是怎样使用的?

        递归锁,在进行多线程同步的时候,遇到锁重入问题就可以采用递归锁。
 

线程安全问题

线程锁的实现机制其实基于很简单的概念:标志是否被占用

  1. OSSpinLock : 自旋锁,效率高,占用CPU,废弃了,会产生优先级反转,导致优先级较高的线程时间更多,而被锁线程时间更少
  2. dispatch_semaphore : 信号量, 信号量数值觉得最多能进入的线程树,一般设置为1.
  3. NSLock : 互斥锁,对pthread普通锁的封装
  4. NSRecursiveLock : 递归锁,也是互斥锁, 解决递归调用死锁问题.对pthread递归锁的封装.
  5. NSCondition : 对pthread条件与锁的封装.
  6. NSConditionLock : 对NSCondition的进一步封装,可以设置具体的条件.
  7. @synchorized : 同步,可以对代码块使用.
  • 互斥锁应当是排它的,意思是锁在被某个线程获取之后,只有获取锁的线程才能释放这个锁。其他线程必须等到获取锁的线程不再拥有锁之后,才能继续执行。

  • 信号量拥有比互斥锁更多的用途。当信号量的value大于0时,所有的线程都能访问临界资源。在线程进入临界区后,value减一,反之亦然。如果信号量初始化为0时,可以看做是等待任务执行完成而非资源保护。value的操作应当是采用原子操作来保证指令的安全性的

  • 自旋实现的线程锁来说,存在一个线程间共享的标记变量。当某个线程进入临界区后,变量被标记,此时其他线程再想进入临界区,会进入while循环中空转等待

  • 线程锁的实现机制其实基于很简单的概念:标志是否被占用


问题5: GCD如何实现最大并发数呢?

        ​​​​​​​GCD 可以用信号量 semaphore 来实现

//建立任务组
    dispatch_group_t group = dispatch_group_create();
    //设置信号量大小
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);

    //创建全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //添加实现任务
     for (int i = 0; i < 100; i++) {
    //semphore值为0时会一直等待执行。当>=1时执行下面的代码。可通过一次,会将semphore的值减1.
          dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    //监视queue队列
          dispatch_group_async(group, queue, ^{

    NSLog(@"这里在执行耗时操作----%i",i);
                sleep(2);
    //当前线程执行完成后,使semaphore的值加1,这样,如果semphore的值就加1,会触发dispatch_semaphore_wait执行一条代码
                dispatch_semaphore_signal(semaphore);
         });
     }
    //永远地监听Group中的任务是否执行完成。
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值