美文网首页iOS面试总结iOS
GCD多线程问题整理

GCD多线程问题整理

作者: oc123 | 来源:发表于2019-04-19 10:52 被阅读13次
    1.GCD队列有哪几种类型?有哪几种队列?

    GCD队列分为串行队列、并行队列两种类型;队列有主串行队列、全局并行队列(在系统主线程空闲时才会执行)、自定义队列;

    2.如何用GCD同步若干个异步调用(如根据若干个URL异步加载多张图片,然后在都下载完成后合成一张整图)?

    使用Dispatch Group追加block到Global Group Queue,这些block全部执行完毕,回到主线程;

        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        dispatch_group_t group = dispatch_group_create();
        dispatch_group_async(group, queue, ^{
            /**加载图片1*/
            dispatch_sync(queue, ^{
                NSLog(@"加载图片1");
                [self load1];
            });
        });
        dispatch_group_async(group, queue, ^{
            /**加载图片2*/
            dispatch_sync(queue, ^{
                NSLog(@"加载图片2");
                [self load2];
            });
        });
        dispatch_group_async(group, queue, ^{
            /**加载图片3*/
            dispatch_sync(queue, ^{
                NSLog(@"加载图片3");
                [self load3];
            });
        });
        
    //    //等待上面的任务全部完成后,会往下继续执行 (会阻塞当前线程)
    //    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        
        //等待上面的任务全部完成后,会收到通知执行block中的代码 (不会阻塞线程)
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            /**合并图片*/
            NSLog(@"Method1-全部任务执行完成");
        });
    
    - (void)load1 {
        sleep(2);
        NSLog(@"加载图片1,沉睡2s");
    }
    - (void)load2 {
        sleep(5);
        NSLog(@"加载图片2,沉睡5s");
    }
    - (void)load3 {
        sleep(8);
        NSLog(@"加载图片3,沉睡8s");
    }
    

    dispatch_group_wait :在任务组完成时调用,或者任务组超时是调用(完成指的是enter和leave次数一样多,leave多崩溃,enter多未完成)
    dispatch_group_notify:不管超不超时,只要任务组完成,会调用,不完成不会调用
    dispatch_group_async(group, queue, block) 和 dispatch_group_notify(group, queue, block) 组合在执行同步任务时正常,在执行异步任务时不正常。

    3.GCD怎么保证任务的按顺序执行,等待一些任务完成后才能继续进行?

    使用dispatch_barrier_async;在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用barrier来等待之前任务完成,避免数据竞争等问题;

    4.苹果为什么废弃dispatch_get_current_queue?

    dispatch_get_current_queue容易造成死锁,执行线程时,会产生一个与之绑定的队列,而执行dispatch_get_current_queue时,产生的队列可能与该线程没有关系,所以可能造成死锁;

    5.dispatch_after的作用?

    dispatch_after是来延迟执行的GCD方法,dispatch_after能让我们添加进队列的任务延迟执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加到自己需要的队列中去执行。(在对时间要求非常精准的情况下才可能会出现问题。)dispatch_after参数:第一个参数是time,第二个参数是想加入那个队列,第三个参数需要执行的block。

    6.GCD如何创建串行队列、并行队列?(同步、异步线程的创建略)

    dispatch_queue_t queue = dispatch_queue_create("队列名",队列类型(DISPATCH_QUEUE_SERIAL -串行队列,DISPATCH_QUEUE_CONCURRENT -并行队列) );

    7.GCD之线程怎么形成死锁的?

    参考自:https://www.cnblogs.com/LDSmallCat/p/4910080.html
    正确代码如下:(同步:不会立即返回,即阻塞当前线程,等待block同步执行完成;异步:立即返回, block会在后台异步执行

    -(void)testThreadDeadlock {
    ①    NSLog(@"0 ---- %@", [NSThread currentThread]);
    ②    // 创建一个串行队列
    ③    dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL);
    ④    dispatch_async(queue, ^{// block1开始
    ⑤       NSLog(@"1 ---- %@", [NSThread currentThread]);
    ⑥       
    ⑦        dispatch_async(queue, ^{// block2开始
    ⑧            NSLog(@"2 ---- %@", [NSThread currentThread]);
    ⑨        });// block2结束
    ⑩        
    ⑪        NSLog(@"3 ---- %@", [NSThread currentThread]);
    ⑫    });// block1结束
    ⑬    NSLog(@"4 ---- %@", [NSThread currentThread]);
    }
    

    案例分析:
    1.调用testThreadDeadlock方法执行到①时没有开启线程,在主线程中打印;
    2.执行到④时检测到异步函数async,此时async函数直接返回,不会等函数中的block1执行完毕才返回,执行到④时系统有两件事要做,第一跳至⑬行打印,第二开启一个新的线程1(是子线程而非主线程,假设是子线程1)执行block1当中的内容。所以看到主线程先打印NSLog(@"0 ---- %@", [NSThread currentThread]);、NSLog(@"4 ---- %@", [NSThread currentThread]);,打印结束就没有主线程的事情了,接下来子线程登场;
    3.子线程1执行block1的内容:\color{#FF0000}{1.}执行⑤行打印,在(子线程1)打印;\color{#FF0000}{2.}执行到⑦行检测到async异步函数,直接返回,不等待block2执行完毕,程序继续向下执行,即打印⑪行NSLog(@"3 ---- %@", [NSThread currentThread]);(子线程1打印),同时又创建一个 (子线程2)
    4.现在分析一下程序运行到这里一共经历了几个线程?3个,主线程1个,④行、⑦行分别创建了两个子线程1和2;(怎么看是否创建了新的线程?"一般"来说看函数,是async异步执行还是sync同步执行,为什么要用"一般"?往下看)

    那么问题来了,⑤行和⑪行的打印(即1、3打印)都在一个线程上我们可以理解(看线程地址),但是为什么⑧行的打印也和这两个在同一个线程呢?因为④行创建了一个子线程打印了⑤行和⑪行之后,这个线程的任务结束了,正常来说就要销毁了,没他什么事了。但是我们在⑦行又需要创建一个线程,创建线程需要时间和空间成本(占用内存)所以在子线程1执行完任务进入线程池要销毁的时候,发现系统还要再创建一个线程,ok那我就不销毁了,你也不用再创建了,所以线程1就被重复利用了,所以他们在一个线程上打印。

    5.回到正题,在子线程1执行⑤行、⑪行的打印之后,被系统重复利用,来执行⑦行block2的内容,⑧行打印结束一个消息循环结束,进入休眠等待下次触发。
    6.总结:为什么程序的打印结果会是这样的?与两个因素有关:\color{#FF0000}{1.}函数是async异步执行还是sync同步执行;\color{#FF0000}{2.}队列我们创建的是串行队列这个串行队列里面有两个任务block1block2,串行队列遵循先进先出原则,block1在④行加入,先执行,block2在⑦行加入后执行。block1中有三个任务⑤行、⑪行打印和block2,且block2在两个打印中间,按照程序自上到下执行…问题又来了,为什么block2不是在两个打印任务之间执行?而是在打印结束后执行?这时就要看函数啦~
    async这是什么?异步执行!要开启新的线程(或是重复利用线程池中已经创建的线程,什么样的线程可以重复利用?这个线程已经执行完它的任务,进入线程池马上销毁的线程才可以重复利用)(开启线程或是获取线程池中重复利用的线程是需要时间成本的)检测到这个函数怎么办?直接返回,不用等这个函数的block执行完就返回。什么意思?执行到⑦行的时候兵分两路,一路向下执行⑪行打印,一路从线程池中获取重复利用的线程处理block2的任务。在兵分两路的时候子线程1还没有处理完⑪行的打印,它的任务没有结束啊!为什么处理block2没有创建新的线程?因为需要时间和空间成本。GCD为我们做了优化,(怎么优化的我们不用操心~~看不到源码这是个迷,不过看到了也不一定看得懂!哈哈所以不创建新的线程,等子线程1执行完他的任务,我们重复利用它!ok说完了;
    综上:如果⑦行改为dispatch_sync同步操作,将会造成死锁,理由:执行dispatch_sync同步线程时,不会创建新线程,那么在⑦行将调用线程1,同时block1向下执行⑪行代码也要调用线程1,造成死锁;

    8.分析自旋锁、互斥锁、NSLock、信号量、条件锁、对象锁 、递归锁?

    自旋锁OSSpinLock停止使用,线程的优先级反转可能导致线程死锁,使用os_unfair_lock_t(实质为互斥锁)代替;
    所有锁(包括NSLock)的接口实际上都是通过NSLocking协议定义的,它定义了lockunlock方法。你使用这些方法来获取释放该锁。

    锁类型 概念描述 优劣对比
    自旋锁 自旋锁(OSSpinLock)
    创建锁:_pinLock = OS_SPINLOCK_INIT;
    加锁:OSSpinLockLock(&_pinLock);
    解锁:OSSpinLockUnlock(&_pinLock);
    为实现保护共享资源而提出的一种锁机制。互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
    在任何时刻,最多只能有一个保持者,都能保证线程安全。调度机制上,如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
    互斥锁 互斥锁(pthread_mutex C语言)不能由多个线程同时执行
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    定义锁的类型(属性):pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    pthread_mutex_t mutex;
    创建锁:pthread_mutex_init(&mutex, &attr);
    申请锁:pthread_mutex_lock(&mutex);
    {临界区}
    释放锁:pthread_mutex_unlock(&mutex);
    互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。锁的类型,可以有 PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE 等等;如果lock区域代码,再次进行加锁,就会变成递归,因此我们要把锁的属性变成PTHREAD_MUTEX_RECURSIVE来避免死锁
    在任何时刻,最多只能有一个保持者,都能保证线程安全。调度机制上,如果共享数据已经有其他线程加锁了,线程会进入休眠状态等待锁。一旦被访问的资源被解锁,则等待资源的线程会被唤醒。如果临界区很短,忙等的效率也许更高;由于 pthread_mutex 有多种类型,可以支持递归锁等,因此在申请加锁时,需要对锁的类型加以判断,这也就是为什么它和信号量的实现类似,但效率略低的原因。底层的api,复杂的多线程处理建议使用,并且可以封装自己的多线程
    信号量 信号量(dispatch_semaphore)互斥类型
    锁是服务于共享资源的;而semaphore是服务于多个线程间的执行的逻辑顺序的。信号量的本质就是调度线程!
    dispatch_semaphore_create(value) value是初始信号持有量
    dispatch_semaphore_signal 信号量+1
    dispatch_semaphore_wait 信号量-1
    如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
    semaphore是通过一个值来实现线程的调度的,因此借助这种机制,我们也可以实现对线程数量的限制休眠,需要上下文切换,耗时,占用时间比较长时候使用注意:不能在中断中休眠!
    条件锁 条件锁(NSConditionLock)
    互斥锁pthread_mutex的一种封装
    1
    对象锁 对象锁(NSLock)
    对pthread_mutex锁的属性为normal的封装;
    互斥锁 不能多次调用lock方法,会造成死锁
    1
    递归锁 递归锁(NSRecursiveLock)
    对pthread_mutex锁的属性为recursive的封装;
    递归锁的性能出奇的高,但是只能作为递归使用,所以限制了使用场景;

    上下文切换的概念:对线程的上下文进行保存和恢复的过程(当发生操作系统事件时,如唤起系统调用等情况,内核会切换执行路径;执行中路径的状态,如CPU寄存器等信息会保存到各自路径专用的内存块中;从切换目标路径专用的内存块中,复原CPU寄存器等信息,继续执行切换路径的CPU命令;)

    各锁的性能对比图:


    锁性能对比图.png

    NSCondition:使用其做多线程之间的通信调用不是线程安全的
    NSConditionLock:单纯加锁性能非常低,比NSLock低很多,但是可以用来做多线程处理不同任务的通信调用

    9. GCD、NSOperatinQueue、NSThread对比
    GCD NSOperatinQueue NSThread
    解析 基于C语言框架,充分利用多核,苹果推荐的多线程技术 面向对象多线程编程
    本质是对GCD的封装
    轻量级多线程编程,真正的多线程编程技术,每一个NSThread对象对应一个线程
    抽象程度
    优点 基于C语言,是替代前两者的高效强大的技术,执行效率比前两者高,代码比较集中易维护 不用担心线程管理、同步的事情,主要精力在对线程的操作(暂停、继续和取消)上,可规定最大并发数;是面向对象的,建立在GCD上,在架构上、优先级管理上做的比GCD好,可以很好支持GCD没有或者不完美的操作,比如取消线程、KVO监控、指定操作间的优先级或相互依赖 量级轻,使用简单
    缺点 - 相对GCD来说代码比较分散,不容易管理 需要自己管理线程的生命周期、线程同步、睡眠、唤醒等,对线程加锁需要耗费一定系统开销
    10.进程与线程的关系

    进程概念: 进程是程序在计算机的一次执行活动,打开一个app,就开启了一个进程,可包含多个线程;
    线程概念: 独立执行的代码段,一个线程同时间只能执行一次,反之多线程并发可以同一时间执行多个任务;

    线程与进程的区别和联系?

    • 线程是进程的基本单位。
    • 进程和线程都是由操作系统所产生的,程序运行的基本单元,系统利用该基本单元实现系统对于应用的并发性。
    • 进程和线程的主要差别,在于它们是不同的操作系统资源管理方式
    • 进程有独立的地址空间,一个进程崩溃后,在保护模式下,不会对其他进程产生影响。
    • 线程只是一个进程中的不同执行路径。
    • 线程有自己的堆栈和局部变量,但线程之间没有独立的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
    • 但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

    使用多线程的优势:
    1.使用多线程可以减少程序的响应时间。在单线程(单线程指的是程序执行过程中只有一个有效操作的序列,不同操作之间都有明确的执行先后顺序)的情况下,如果某个操作很耗时,或者陷入长时间的等待(如等待网络响应),此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时的线程分配到一个单独的线程去执行,使得程序具备了更好的交互性。
    2.与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程的代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
    3.多CPU或多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
    4.使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的程序可以分成多个线程来执行。

    11. dispatch_apply

    dispatch_apply函数是dispatch_sync函数和Dispatch Group的关联API,该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束。可以实现对数组array[]的遍历。

    NSArray *array =@[@"",@"",@""];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([array count], queue, ^(size_t index){
        NSLog(@"%zu: %@", index, [array objectAtIndex:index]); 
    });
    
    12. dispatch_set_target_queue

    用dispatch_queue_create函数生成的Dispatch Queue都是默认优先级的线程,要改变线程的优先级,dispatch_set_target_queue的作用就凸显出来了,栗子如下:

    dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
    dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);
    

    不仅如此,该函数还可以改变执行层次,最底层的Serial Dispatch Queue和Concurrent Dispatch Queue在按照顺序在上一层的Serial Dispatch Queue中执行,避免最底层线程并行执行。

    13.利用NSOperation与NSOperationQueue处理多线程时,有3个NSOperation分别为A,B,C,要求A,B执行完之后,才执行C,如何做?

    对于queue中的operations改变执行顺序的取决2点:
    1.NSOperation 对象的依赖关系和queue的最大并发operation数量决定;
    2.NSOperation 的优先级确定;优先级则是NSOperation 对象的一个属性。默认所有的“普通”优先级,通过setQueuePriority方法提升或者降低NSOperation的优先级,优先级只能应用于相同的queue的NSOperation,如果应用有多个NSOperationqueue,每个queue优先级等级是相互独立的。

        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue setMaxConcurrentOperationCount:1];
        NSBlockOperation *A= [NSBlockOperation blockOperationWithBlock:^(){
            NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]);
        }];
        NSBlockOperation *B = [NSBlockOperation blockOperationWithBlock:^(){
            NSLog(@"执行第2次操作,线程:%@", [NSThread currentThread]);
        }];
        NSBlockOperation *C = [NSBlockOperation blockOperationWithBlock:^(){
            NSLog(@"执行第3次操作,线程:%@", [NSThread currentThread]);
        }];
        [C setQueuePriority:NSOperationQueuePriorityVeryLow];
        // C依赖于A,B
        [C  addDependency:A];
        [C  addDependency:B];
        [queue addOperation:A];
        [queue addOperation:B];
        [queue addOperation:C];
    // AB并行执行,顺序随机,最后执行C
    
    14.多线程的底层实现?

    1.线程:Mach是第一个以多线程方式处理任务的系统,因此多线程的底层实现机制是基于Mach的线程;
    2.开发中很少用Mach级的线程,因为Mach级的线程没有提供多线程的基本特征,线程之间是独立的;
    3.开发中实现多线程的方案:
        (1) C语言的POSIX接口(可移植操作系统接口):#include<pthread.h>
        (2) OC的NSThread
        (3) C语言的GCD接口(性能最好,代码更简洁)
        (4) OC的NSOperation和NSOperationQueue(基于GCD)

    15.多线程的三大特性:

    1.原子性:线程的一个或者多个操作要么全部执行,且执行过程不会被打断,要么全部都不执行;
    2.可见性:可见性是指多个线程访问同一个变量的时候,一个线程修改了这个变量的值,其他线程也可以立刻看到这个修改后的值。Java提供关键字volatile来保证可见性;锁也能保证可见性;
    3.有序性:即程序的执行顺序按照代码的先后顺序执行。

    16.volatile关键字的作用?

    volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

    17.dispatch_suspend、dispatchp_resume线程挂起与恢复

    简单来说,就是可以暂停、恢复队列上的任务。但是这里的“挂起”,并不能保证可以立即停止队列上正在运行的block;dispatch_suspend并不会立即暂停正在运行的block,而是在当前block执行完成后,暂停后续的block执行。

    18.列举Cocoa中常见的几种多线程的实现,并谈谈多线程安全的几种解决办法?

    GCD、NSOperation、NSThread;自旋锁、互斥锁、信号量;

    19.如果让你实现 GCD 的线程池,讲一下思路

    线程池包含如下7个部分:1、线程池管理器(用于创建并管理线程池,单例);2、工作线程(线程池中的线程);3、任务接口(每个任务必须实现的接口,以供工作线程调度任务的执行);4、任务队列(用于存放没有处理的任务,提供一种缓冲机制);5、corePoolSize核心池的大小(默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数量达到corePoolSize后,就会把达到的任务放到缓存队列当中);6、maximumPoolSize线程池最大线程数(它表示在线程池中最多能创建多少个线程);7、keepAliveTime存活时间(表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,这时如果一个线程空闲的时间达到keepAliveTime,则会终止直到线程池中的线程数不大于corePoolSize);
    具体流程如下:
    1、当通过任务接口向线程池管理器中添加任务时,如果当前线程池管理器中的线程数目小于corePoolSize,则每来一个任务,就会通过线程池管理器创建一个线程去执行这个任务;
    2、如果当前线程池中的线程数目大于等于corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;
    3、如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
    4、如果线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;

    20.Global Dispatch Queue全局队列的四种执行优先级?

    高优先级(High Priority,参数:DISPATCH_QUEUE_PRIORITY_HIGH)、默认优先级(Default Priority,参数:DISPATCH_QUEUE_PRIORITY_DEFAULT)、低优先级(Low Priority,参数:DISPATCH_QUEUE_PRIORITY_LOW)、后台优先级(Background Priority,参数:DISPATCH_QUEUE_PRIORITY_BACKGROUND)

    21.使用GCD以及block时要注意些什么?它们两是一回事儿么?block在ARC中和传统的MRC中的行为和用法有没有什么区别,需要注意些什么?

    block的使用需注意:1、block的内存管理;2、防止循环强引用;
    (1、block 在实现时就会对它引用到的,它所在方法中定义的栈变量进行一次只读拷贝,然后在 block 块内使用该只读拷贝;2、非内联(inline) block 不能直接访问 self,只能通过将 self 当作参数传递到 block 中才能使用,并且此时的 self 只能通过 setter 或 getter 方法访问其属性,不能使用句点式方法。但内联 block 不受此限制;)
    不是一回事;
    block避免循环强引用需要注意:1、非ARC(MRC):__block修饰的变量的引用计算是不变的(还有修改外部临时变量);2、ARC:__weak/__unsafe_unretained

    22.线程之间是如何通信的?

    NSThread类提供了两个比较常用的方法,用于实现线程间的通信,这两个方法的定义格式如下:

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
    
    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
    

    NSMachPort:(基本机制:A线程父线程创建NSMachPort对象,并加入A线程的runloop。当创建B线程辅助线程时,将创建的NSMachPort对象传递到主体入口点,B线程辅助线程就可以使用相同的端口对象将消息传回A线程父线程。)

    23.GCD内部怎么实现的?

    1.iOS和OS X的核心是XNU内核,GCD是基于XNU内核实现的;
    2.GCD的API全部在libdispatch库中;
    3.GCD的底层实现主要有Dispatch Queue管理block(操作)和Dispatch Source处理事件(MACH端口发送,MACH端口接收,检测与进程相关事件等10种事件)。

    24.NSBlockOperation和NSInvocationOperation用法的区别?

    NSBlockOperation执行代码块,NSInvocationOperation执行指定的方法,相对来说后者更加灵活易用。

    25.UIKit类要在哪一个应用线程上使用?

    UIKit的界面类只能在主线程上使用,对界面进行更新,多线程环境中要对界面进行更新必须要切换到主线程上。

    26.GCD中栅栏机制?

    栅栏函数,只能用在调度并发队列中,不能使用在全局并发队列中。
    1、实现高效率的数据库访问和文件访问。
    2、避免数据竞争。
    dispatch_barrier_async函数会等待追加到并行队列上的并行执行的处理全部结束之后,再将制定的处理追加到该并行队列中。然后再由dispatch_barrier_async函数追加的处理执行完毕后,并行队列才恢复为一般的动作,追加到该并行队列的处理又开始执行。

    27.线程共享进程资源的实例?

    火车票抢票案例。

    28.unix上进程怎么通信?

    UNIX主要支持三种通信方式:
    1. 基本通信:主要用来协调进程间的同步和互斥;(1.锁文件通信:通信的双方通过查找特定目录下特定类型的文件(称锁文件)来完成进程间,对临界资源访问时的互斥;例如进程p1访问一个临界资源,首先查看是否有一个特定类型文件,若有,则等待一段时间再查找锁文件。2.记录锁文件
    2. 管道通信:适应大批量的数据传递;
    3. IPC:适应大批量的数据传递;

    29.列举几种进程的同步机制、进程的通信途径、死锁及死锁的处理方法。

    进程的同步机制:原子操作、信号量机制、自旋锁、管程、会合、分布式系统(具体解释可参照:https://blog.csdn.net/opah521/article/details/6546410
    进程之间通信的途径:共享存储、系统消息传递系统、管道(以文件系统为基础)
    进程死锁的原因:资源竞争及进程推进顺序非法(进程在运行过程中,请求和释放资源的顺序不当
    死锁的4个必要条件:互斥、请求保持、不可剥夺、环路;
    死锁的处理:鸵鸟策略、预防策略、避免策略、检测与解除死锁;

    30.什么是线程同步,如何实现的?子线程回到主线程的方法是什么,有什么作用?

    线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
    回到主线程的方法:dispatch_async(dispatch_get_main_queue(), ^{});
    作用:刷新UI界面。

    31.使用atomic一定是线程安全的吗?

    不一定是线程安全的;atomic和nonatomic修饰的OC对象,系统都会自动生成setter/getter方法,区别在于atomic会进行加锁操作,nonatomic不会。系统默认使用atomic(在多线程环境下,解析的访问器提供一个对属性的安全访问,从获取器得到的返回值或者通过设置器设置的值可以一次完成,即便是别的线程也正在对其进行访问。)。
    atomic的本意是指属性的存取方法线程安全的(thread safe),并不保证整个对象是线程安全的。

    A、B、C等多个线程都要操作同一个对象setter,D线程要getter这个对象的值,那么每个线程都成保证各自数据的完整性,但是D线程最后get到的值并不能确定。

    所以atomic能够保证数据的完成性,也就是说他只是读写安全,并不能准确定义说他是线程安全的。因为线程可以对数据做很多操作,包括读写,还有release、retain,假如说对一个已经释放的对象进行release,就会导致crash。

    32.OC中异步使用的哪种事件模型,iOS中异步实现机制?

    异步非阻塞I/O(AIO)模型。不是很确定,自我理解为,当需要异步调用时,开启一个异步线程,在这个子线程中进行操作;

    33.GCD的queue、main queue中执行的代码一定是在main thread么?

    不一定。对于queue中所执行的代码,不一定在main thread中;如果queue是在主线程中创建的,那么在主线程中执行;如果在子线程中创建,那么不在main thread中执行。对于main queue就是在主线程中的,因此一定会在主线程中执行。

    34.有a、b、c、d 4个异步请求,如何判断a、b、c、d都完成执行?如果需要a、b、c、d顺序执行,该如何实现?

    活学活用,dispatch_group_async判断4个同步请求都执行完成,然后执行dispatch_group_notify来回调;或者加锁、信号量、栅栏。顺序执行,NSOperation通过添加依赖关系、dispatch_barrier_async栅栏、加锁、信号量、串行队列等。

    35.如何实现dispatch_once?

    参考:http://www.dreamingwish.com/article/gcd-guide-dispatch-once-2.html
    dispatch_once的主要处理的情况如下:(通过使用信号量来实现)
    1.线程A执行Block时,任何其它线程都需要等待。
    2.线程A执行完Block应该立即标记任务完成状态,然后遍历信号量链来唤醒所有等待线程。
    3.线程A遍历信号量链来signal时,任何其他新进入函数的线程都应该直接返回而无需等待。
    4.线程A遍历信号量链来signal时,若有其它等待线程B仍在更新或试图更新信号量链,应该保证此线程B能正确完成其任务:a.直接返回 b.等待在信号量上并很快又被唤醒。
    5.线程B构造信号量时,应该考虑线程A随时可能改变状态(“等待”、“完成”、“遍历信号量链”)。
    6.线程B构造信号量时,应该考虑到另一个线程C也可能正在更新或试图更新信号量链,应该保证B、C都能正常完成其任务:a.增加链节并等待在信号量上 b.发现线程A已经标记“完成”然后直接销毁信号量并退出函数。
    总结:无锁的线程同步编程非常精巧,为了提升效率,每一处线程竞争都必须被考虑到并妥善处理。

    36.关于NSOperation
    • NSOperation:抽象类,不能直接使用,需要使用其子类(类似的类还有核心动画)。
    • 两个常用子类:NSInvocationOperation(调用方法)和NSBlockOperation(代码块)。
    • 两者没有本质区别,后者使用Block的形式组织代码,使用相对方便。
    • NSInvocationOperation在调用start方法后,不会开启新的线程只会在当前线程中执行。
    • NSBlockOperation在调用start方法后,如果封装的操作数>1会开辟多条线程执行,=1只会在当前线程执行。
    • NSOperationQueue创建的操作队列默认为全局队列,队列中的操作执行顺序是无序的,如果需要让它有序执行需要添加依赖关系。
    // 操作op3依赖于操作op2
    [op3 addDependency:op2];
    // 操作op2依赖于操作op1
    [op2 addDependency:op1];
    
    • 同时可以设置最大并发数。
    • NSOperationQueue、NSOperation支持取消、暂停的操作,但是正在进行的操作并不能取消,这一旦取消不可恢复。
    • NSOperationQueue支持KVO,可以监测Operation是否正在执行(isExecuted)、是否结束(isFinished),是否取消(isCancelled)。
    37.NSOperationQueue是队列吗?

    存放NSOperation的集合类,不能说是队列,因为不是严格的先进先出。

    38.为什么要取消/恢复队列呢?

    1.一般在内存警告后取消队列中的操作;2.为了保证scrollView在滚动的时候流程,通常在滚动开始时,暂停队列中的所有操作,滚动结束后,恢复操作。

    39.下面关于线程管理错误的是?

    A.GCD所用的开销要比NSThread大
    B.可以在子线程中修改UI元素
    C.NSOperationQueue是比NSThread更高层的封装
    D.GCD可以根据不同优先级分配线程

    • 参考答案:B
    • 解析:首先,UI元素的更新必须在主线程。GCD与Block配合使用,block需要自动捕获上下文变量信息等,因此需要更多的资源,故比NSThread开销要大一些。NSOperationQueue与NSOperation配合使用,比NSThread更易于操作线程。GCD提供了多个优先级,我们可以根据设置优先级,让其自动为我们分配线程。
    40.

    相关文章

      网友评论

        本文标题:GCD多线程问题整理

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