2022iOS面试题之多线程

GCD特点:
1、GCD是基于c语言的用于多核的并行运算
2、GCD会自动利用更多的CPU内核(比如双核、四核)
3、GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
4、程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

队列串行队列:会顺序执行
         并行队列:可以并行执行
         全局队列:系统创建,全局并发队列
         主队列:主队列与主线程是绑定的

只要是同步的方式提交任务,无论是串行还是并发,就会在同一线程去执行。
同步:只能在当前线程中执行任务,不具备开启新线程的能力
异步:可以在新线程中执行任务,具备开启新线程的能力
———————————————————————————————
死锁

原因:主队列里sync 同步执行,就是会先阻塞当前线程,直到block 当中的代码执行完毕,但是block在viewdidload里面,block需要等待viewdidload执行完结束才能继续,但是viewdidload需要等待block执行完才能结束。
可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。

- (void)viewDidLoad {
    [super viewDidLoad];
   //会死锁,都在主队列里同步执行
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"deallock");
    });
}
解决
- (void)viewDidLoad {
    [super viewDidLoad];
    //不会死锁,serialQueue虽然是串行队列,但不是主队列,serialQueue和viewDidLoad(在主队列执行)的执行队列不同就不会等待,
    dispatch_sync(serialQueue, ^{
        NSLog(@"deallock");
    });
}


———————————————————————————————
面试题:多读单写的GCD实现?异步栅栏到并发队列方案[就是像筑起一个栅栏一样,将队列中的多组线程任务分割开]

-(id)objectForKey: (NSString *) key{
  __block id obi:
  dispatch_queue_t concurrent_queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);   
//同步读取指定数据,单读   dispatch_sync(concurrent_queue, ^{     obj= [userCenterDic obiectForKey: key];
  });
  return obi;
} -(
void)setobject: (id)obj forKey: (NSString *) key{
  dispatch_queue_t concurrent_queue = dispatch_queue_create("barrier", DISPATCH_QUEUE_CONCURRENT);
 //异步栅栏调用设置数据,多写   
  dispatch_barrier_async (concurrent_queue,
^{  
   [userCenterDic setobiect:obi forKey: key];   
  });
}
区别?
1、dispatch_barrier_sync需要等待自己的任务(barrier)结束之后,才会继续添加并执行写在barrier后面的任务(4、5、6),然后执行后面的任务
2、dispatch_barrier_async将自己的任务(barrier)插入到queue之后,不会等待自己的任务结束,它会继续把后面的任务(4、5、6)插入到queue,然后执行任务。

注意:在使用栅栏函数时.使用自定义队列才有意义,如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用。

—————————————————————————————————
面试题:A/B/C任务并发执行完成后,再调用D任务?
在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。
使用dispatch_group 队列组。

dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT);
 
    dispatch_group_t group = dispatch_group_create();
    
    for (NSInteger i = 0; i < 10; i++) {
        
        dispatch_group_async(group, concurrentQueue, ^{
            
            sleep(1);
            
            NSLog(@"%zd:网络请求",i);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        NSLog(@"刷新页面");
    });

——————————————————————————————
使用信号量,控制多线程下的同步操作。(作用:保证关键代码段不被并发调用。)

  1. 初始化信号(initialize/create)
  2. 发信号(signal/post)
  3. 等信号(wait/suspend)
  4. 释放信号(destroy)

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"任务1:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务2:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务3:%@",[NSThread currentThread]);
    });


———————————————————NSOperation——————————————

特性:

1.Dependency 设置依赖,优先级
2.NSOperation 有如下几种的运行状态:状态控制
* Pending
* Ready
* Executing
* Finished
* Canceled
除 Finished 状态外,其他状态均可转换为 Canceled 状态。
3. maxConcurrentOperationCount,默认的最大并发 operation 数量是由系统当前的运行情况决定的(来源),我们也可以强制指定一个固定的并发数量.
4、可以很方便的取消一个操作的执行。
5、使用KVO观察对操作执行状态的更改:isExecuting、isFinished、isCancelled。

NSOperation的任务状态

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

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

isFinished 当前任务是否已执行完毕

isCancelled 当前任务是否已取消


怎么去控制状态?重写main方法的话,底层帮我们自动控制的。重写start方法才是我们控制状态

1.如果只重写了NSOperation的main方法,底层会为我们控制变更任务执行完成状态,以及任务退出(后续线程的退出和NSOperation的退出)

2.如果重写了NSOperation的start方法,需要我们自行控制任务状态,在合适的时机去修改对应的isFinished等
 

    查看NSOperation的start方法源码,理解上面两点
    start方法内,首先创造一个自动释放池,然后获取线程优先级
    做一系列的状态异常判断,然后判断当前状态是否isExecuting
    如果不是,那么我们手动变成isExecuting,然后判断当前任务是否有被取消
    若未被取消就调用NSOperation的main方法
    再之后,调用NSOperation的finish方法。finish 方法中:在内部通过KVO的方式去变更isExecuting状态为isFinished状态
    之后调用自动释放池的release 

所以系统是在start方法里面为我们维护了任务状态的变更,若重写start,则没人帮我们维护了,只能自己手动维护

kvo监听状态属性的变化,来确定是否需要移除的,通过 KVO 监测 isExecuting 和 isFinished 这几个变量,来监测 Operation 的完成状态的

//设置优先级最高
    op1.qualityOfService = NSQualityOfServiceUserInteractive;

 //设置依赖 : 列如:下载 解压  升级完成
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
            NSLog(@"下载");
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"解压");
    }];
    NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
             NSLog(@"升级完成");
    }];
    //设置操作间的依赖
    [op2 addDependency:op1];
    [op3 addDependency:op2];
    //会发生循环依赖  ,什么都不操作
    //操作添加到队列中
    [self.queue addOperations:@[op1,op2] waitUntilFinished:NO];
    //依赖关系可以跨队列
    [[NSOperationQueue mainQueue] addOperation:op3];


GCD 与 NSOperation 的对比

* 首先要明确一点,NSOperationQueue 是基于 GCD 的更高层的封装.
* 从易用性角度,GCD 由于采用 C 风格的 API,在调用上比使用面向对象风格的 NSOperation 要简单一些。
* 从对任务的控制性来说,NSOperation 显著得好于 GCD,和 GCD 相比支持了 Cancel 操作(注:在 iOS8 中 GCD 引入了 dispatch_block_cancel 和 dispatch_block_testcancel,也可以支持 Cancel 操作了),支持任务之间的依赖关系,支持同一个队列中任务的优先级设置,同时还可以通过 KVO 来监控任务的执行情况。这些通过 GCD 也可以实现,不过需要很多代码,使用 NSOperation 显得方便了很多。
* 效率,直接使用 GCD 效率确实会更高效,NSOperation 会多一点开销,但是通过 NSOperation 可以获得依赖,优先级,继承,键值对观察这些优势
* 从第三方库的角度,知名的第三方库如 AFNetworking 和 SDWebImage 背后都是使用 NSOperation,也从另一方面说明对于需要复杂并发控制的需求,NSOperation 是更好的选择
————————————————————————————————————————————————
线程间通信:切换到主线程[NSOperationQueue mainQueue]

———————————————————NSThread———————————————————————————
NSThread在实际开发中比较常用到的场景就是去实现常驻线程。
将耗时操作放在子线程中,并且在子线程中开启runloop,并使子线程常驻,这样就能不停的执行耗时操作,并且不会影响到主线程啦,滑动tableview很丝滑

   NSThread *thread = [[NSThread alloc] initWithBlock:^{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.35 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"1234567");
        static int count = 0;
        [NSThread sleepForTimeInterval:1];
        //休息一秒钟,模拟耗时操作
    }];;
    [self.timer fire];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}];
[thread start];

如何保证线程安全?
NSOperation的线程安全
和其他多线程方案一样,解决NSOperation多线程安全问题,可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。

@synchronized、
NSLock、
NSRecursiveLock、
NSCondition、
NSConditionLock、
pthread_mutex、
dispatch_semaphore、
OSSpinLock等等各种方式。


@synchronized一般在创建单例对象的时候使用,保证创建的对象是唯一的,但是性能没有dispatch_once_t好。

@implementation XXClass
//@synchronized来实现
+ (id)sharedInstance {
    static XXClass *sharedInstance = nil;
    @synchronized(self) {
        if (!sharedInstance) {
            sharedInstance = [[self alloc] init];
        }
    }
    return sharedInstance;
}


//dispatch_once_t来实现
@implementation XXClass

+ (id)sharedInstance {
    static XXClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
@synchronized 的作用是创建一个互斥锁,保证此时没有其它线程对self对象进行修改,保证代码的安全性。也就是包装这段代码是原子性的,安全的。
这个是objective-c的一个锁定令牌,防止self对象在同一时间内被其他线程访问,起到保护线程安全的作用


1、

atomic:变量默认是有该有属性的,这个属性是为了保证在多线程的情况下,编译器会自动生成一些互斥加锁的代码,避免该变量的读写不同步的问题。  
nonatomic:如果该对象无需考虑多线程的情况,这个属性会让编译器少生成一些互斥代码,可以提高效率。

2、使用GCD实现atomic操作:给某字段的setter和getter方法加上同步队列


3、使用NSLock


4、使用互斥锁
————————————————————————————————————————————————

//互斥锁 -- 保证锁内的代码在同一时间内只有一个线程在执行
        @synchronized (self){}

//使用NSLock

     NSLock* myLock=[[NSLock alloc]init];
    NSString *str=@"hello";
    [NSThread detachNewThreadWithBlock:^{
        [myLock lock];
        NSLog(@"%@",str);
        str=@"world";
        [myLock unlock];
     }];
    [NSThread detachNewThreadWithBlock:^{
        [myLock lock];
        NSLog(@"%@",str);
        str=@"变化了";
        [myLock unlock];
    }];

输出结果不加锁之前,两个线程输出一样 hello;加锁之后,输出分别为hello 与world。


“自旋锁OSSpinLock” & “互斥锁”的异同:
* 共同点
都能够保证线程安全
* 不同点


    互斥锁:如果其他线程正在执行锁定的代码,此线程就会进入休眠状态,等待锁打开;然后被唤醒


    自旋锁:如果线程被锁在外面,那么就会用死循环的方式一直等待锁打开!
    自旋锁OSSpinLock用于轻量级的数据计算,如int型的+1、-1,在内存管理里面,对引用计数器的加减就使用了自旋锁。


递归锁NSRecursiveLock,可以重入,不会造成死锁。
NSLock不可以重入,会造成死锁。

换成递归锁就不会导致死锁了。


线程间通信
对于线程间通信常见的是线程间同步控制,比如通过线程锁、GCD队列、NSOperationQueue操作队列;
若涉及到线程间同步传递数据,最有效的方式是通过共享的进程内存并结合线程锁来控制数据同步;
若涉及到线程间异步传递数据,可通过mach port或者performSelector:onThread:withObject:waitUtilDone:并结合runloop来实现。
传递数据大小而言,对于大容量数据,一般会存储到临时文件并传递文件描述符;
具体的线程间通信方式需要根据实际的使用场景来选择,如同步/异步、传递数据大小、单向/双向通信等。

performSelector实现线程间通信(实现子线程和主线程互相通信,前提是子线程需要保活。【https://www.jianshu.com/p/eae43bdb7eb8】)
NSPort实例 实现线程间通信

案列:图片下载的案列,在子线程下载图片,在主线程更新UI

其他:

PerformSelector 的实现原理?

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

 

posted @ 2022-03-30 09:51  码锋窝  阅读(480)  评论(0编辑  收藏  举报