iOS多线程相关面试题

3,022 阅读10分钟


iOS多线程demo

iOS多线程之--NSThread

iOS多线程之--GCD详解

iOS多线程之--NSOperation

iOS多线程之--线程安全(线程锁)

iOS多线程相关面试题



首先要说明一下,下面所有面试题调用的方法(比如第一个面试题调用的方法是interview1)都是在主线程中调用的。

1. 面试题1

- (void)interview1{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);
        [self performSelector:@selector(test1) withObject:nil afterDelay:.0f];
        NSLog(@"3---%@",[NSThread currentThread]);
    });
}

- (void)test1{
    NSLog(@"2---%@",[NSThread currentThread]);
}

// ***************打印结果***************
2019-12-30 17:37:58.427558+0800 MultithreadingDemo[39113:4277962] 1---<NSThread: 0x600001922d40>{number = 6, name = (null)}
2019-12-30 17:37:58.427659+0800 MultithreadingDemo[39113:4277962] 3---<NSThread: 0x600001922d40>{number = 6, name = (null)}

解释: performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器(即使延时时间是0秒)。由于异步函数dispatch_async是开启一个新的子线程去执行任务,而子线程默认是没有启动Runloop的,所以并不会执行test1方法。

我们可以手动启动runloop来确保test1被调用,也就是在block里面添加一行代码[[NSRunLoop currentRunLoop] run];


如果把异步函数改为同步函数,我们再来看下运行结果:

- (void)interview1{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_sync(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);
        [self performSelector:@selector(test1) withObject:nil afterDelay:.0f];
        NSLog(@"3---%@",[NSThread currentThread]);
    });
}

- (void)test1{
    NSLog(@"2---%@",[NSThread currentThread]);
}

// ***************打印结果***************
2019-12-30 17:47:01.936609+0800 MultithreadingDemo[39150:4282068] 1---<NSThread: 0x6000009660c0>{number = 1, name = main}
2019-12-30 17:47:01.936724+0800 MultithreadingDemo[39150:4282068] 3---<NSThread: 0x6000009660c0>{number = 1, name = main}
2019-12-30 17:47:01.936904+0800 MultithreadingDemo[39150:4282068] 2---<NSThread: 0x6000009660c0>{number = 1, name = main}

解释: 同步函数添加的任务是在当前线程中执行,当前线程就是主线程,而主线程的Runloop是启动的,所以test1会调用。虽然延迟时间时0秒,但是添加到Runloop中的计时器不是立马触发的,而是要先唤醒Runloop,这是需要消耗一定时间的,所以会先打印3再打印2


我们再把performSelector:withObject:afterDelay:替换成performSelector:withObject:看看运行结果:

- (void)interview1{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"1---%@",[NSThread currentThread]);
        [self performSelector:@selector(test1) withObject:nil];
        NSLog(@"3---%@",[NSThread currentThread]);
    });
}

- (void)test1{
    NSLog(@"2---%@",[NSThread currentThread]);
}

// ***************打印结果***************
2019-12-30 17:54:18.072035+0800 MultithreadingDemo[39183:4285659] 1---<NSThread: 0x60000303c300>{number = 3, name = (null)}
2019-12-30 17:54:18.072136+0800 MultithreadingDemo[39183:4285659] 2---<NSThread: 0x60000303c300>{number = 3, name = (null)}
2019-12-30 17:54:18.072215+0800 MultithreadingDemo[39183:4285659] 3---<NSThread: 0x60000303c300>{number = 3, name = (null)}

解释: performSelector:withObject:函数是不涉及到计时器的,所以不会添加到Runloop中,所以是按照1、2、3的顺序执行。

注意:performSelector系列方法中只要是方法名中包含afterDelaywaitUntilDone的都是和计时器有关的,都要注意前面出现的这些问题。

2. 面试题2

- (void)interview2{
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
       NSLog(@"1---%@",[NSThread currentThread]);
    }];
    [thread start];
    
    [self performSelector:@selector(test2) onThread:thread withObject:nil waitUntilDone:YES];
}

- (void)test2{
    NSLog(@"2---%@",[NSThread currentThread]);
}

// ***************运行结果(闪退)***************
2019-12-31 08:36:07.132133+0800 MultithreadingDemo[40268:4493885] 1---<NSThread: 0x6000010d9880>{number = 6, name = (null)}
2019-12-31 08:36:07.432190+0800 MultithreadingDemo[40268:4493455] *** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[Interview performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'

解释: 从运行结果可以看出闪退的原因是target thread exited(目标线程退出)。因为test2方法是在线程thread上执行的,但是线程thread在执行完NSLog(@"1---%@",[NSThread currentThread]);这句代码后就结束了,所以等到执行test2方法时线程thread已经不存在了(严格来说是线程对象是还存在的,只是已经失活了,不能再执行任务了)。

如果想要代码能正常运行,我们可以利用Runloop知识来保活线程。先向当前runloop中添加一个source(如果runloop中一个source、NSTime或Obserer都没有的话就会退出),然后启动runloop。也就是在线程thread的block中添加2行代码,如下所示:

NSThread *thread = [[NSThread alloc] initWithBlock:^{
       NSLog(@"1---%@",[NSThread currentThread]);
        
        // 线程保活
        // 先向当前runloop中添加一个source(如果runloop中一个source、NSTime或Obserer都没有的话就会退出)
        [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSRunLoopCommonModes];
        // 然后启动runloop
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];

3. 面试题3

- (void)interview3{
    NSLog(@"执行任务1--%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"执行任务2--%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务3--%@",[NSThread currentThread]);
}

运行结果:

执行完任务1后就被卡死了。

解释:

interview3方法是在主线程中执行,执行完任务1后,通过同步函数向主队列(串行队列)添加任务2,由于同步添加的任务必须马上执行,而串行队列中当前任务(interview3)还没执行完,就没法安排任务2执行,所以要等当前正在执行的任务(interview3)执行完了后才能执行任务2,而interview3又要等任务2执行完了才会继续往下执行,这样就造成了相互等待而死锁。

4. 面试题4

- (void)interview4{
    NSLog(@"执行任务1--%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"执行任务2--%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务3--%@",[NSThread currentThread]);
}

// ***************打印结果***************
2019-12-31 10:06:37.782135+0800 MultithreadingDemo[41281:4538099] 执行任务1--<NSThread: 0x600003cd1d80>{number = 1, name = main}
2019-12-31 10:06:37.782244+0800 MultithreadingDemo[41281:4538099] 执行任务3--<NSThread: 0x600003cd1d80>{number = 1, name = main}
2019-12-31 10:06:37.782574+0800 MultithreadingDemo[41281:4538099] 执行任务2--<NSThread: 0x600003cd1d80>{number = 1, name = main}

解释:

这和前面一个面试题相比只是把同步函数换成了异步函数。执行完任务1后,通过异步函数添加任务2,虽然异步函数有开启子线程的能力,但是由于是在主队列中,主队列的任务都是在主线程中执行,所以并不会开启子线程。由于是异步函数添加的任务2,所以不必等待任务2就可以继续往下执行,等当前任务(interview4)完成后串行队列再安排执行任务2。所以并不会造成死锁。

5. 面试题5

- (void)interview5{
    NSLog(@"执行任务1--%@",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"执行任务2--%@",[NSThread currentThread]);
        
        dispatch_sync(queue, ^{
            NSLog(@"执行任务3--%@",[NSThread currentThread]);
        });
    
        NSLog(@"执行任务4--%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5--%@",[NSThread currentThread]);

// ***************打印结果(打印1、5、2后卡死)***************    
2019-12-31 10:45:29.071774+0800 MultithreadingDemo[41379:4551961] 执行任务1--<NSThread: 0x6000038460c0>{number = 1, name = main}
2019-12-31 10:45:29.071923+0800 MultithreadingDemo[41379:4551961] 执行任务5--<NSThread: 0x6000038460c0>{number = 1, name = main}
2019-12-31 10:45:29.071932+0800 MultithreadingDemo[41379:4552048] 执行任务2--<NSThread: 0x600003824f40>{number = 6, name = (null)}
}

解释: 首先打印任务1,然后自己创建了一个串行队列,并通过异步函数向这个队列中添加一个任务块(block1),异步函数会开启一个子线程并将block1放入子线程中去执行,开启子线程是要耗时的,而且异步任务不需要等待就可以继续执行它后面的代码,所以打印任务5block1前面执行。

再来看block1任务块,先打印任务2,然后通过同步函数添加的block2任务块需要立马执行,而block1所在的队列是串行队列,block1任务块还没执行完,所以要先等block1执行,而block1又要等block2执行完了才能继续往下执行,所以就造成了相互等待而死锁。

6. 面试题6

- (void)interview6{

    NSLog(@"执行任务1--%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        NSLog(@"执行任务2--%@",[NSThread currentThread]);
        
        dispatch_sync(queue2, ^{
            NSLog(@"执行任务3--%@",[NSThread currentThread]);
        });
        
        NSLog(@"执行任务4--%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5--%@",[NSThread currentThread]);
}

// ***************打印结果*************** 
2019-12-31 11:01:47.812260+0800 MultithreadingDemo[41405:4557566] 执行任务1--<NSThread: 0x600002836180>{number = 1, name = main}
2019-12-31 11:01:47.812470+0800 MultithreadingDemo[41405:4557566] 执行任务5--<NSThread: 0x600002836180>{number = 1, name = main}
2019-12-31 11:01:47.812488+0800 MultithreadingDemo[41405:4557684] 执行任务2--<NSThread: 0x600002830980>{number = 5, name = (null)}
2019-12-31 11:01:47.812567+0800 MultithreadingDemo[41405:4557684] 执行任务3--<NSThread: 0x600002830980>{number = 5, name = (null)}
2019-12-31 11:01:47.812648+0800 MultithreadingDemo[41405:4557684] 执行任务4--<NSThread: 0x600002830980>{number = 5, name = (null)}

解释:

这个和面试题5相比就是新加了一个队列(不管是串行队列还是并发队列都一样),block1任务块和block2任务块分别放在不同的队列中。

先打印任务1再打印任务5和前面是一样的。然后异步函数会开启子线程去执行block1任务块,block1中先打印任务2,然后通过同步函数向另一个队列中添加block2任务块,由于两个block属于不同的队列,block2可以立马被安排执行而不会死锁,所以接着是打印任务3,最后打印任务4

7. 面试题7

- (void)interview7{
    NSLog(@"执行任务1--%@",[NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
        NSLog(@"执行任务2--%@",[NSThread currentThread]);
        
        dispatch_sync(queue, ^{
            NSLog(@"执行任务3--%@",[NSThread currentThread]);
        });
        
        NSLog(@"执行任务4--%@",[NSThread currentThread]);
    });
    
    NSLog(@"执行任务5--%@",[NSThread currentThread]);
}

// ***************打印结果***************
2019-12-31 11:14:27.690008+0800 MultithreadingDemo[41445:4562142] 执行任务1--<NSThread: 0x6000011badc0>{number = 1, name = main}
2019-12-31 11:14:27.690102+0800 MultithreadingDemo[41445:4562142] 执行任务5--<NSThread: 0x6000011badc0>{number = 1, name = main}
2019-12-31 11:14:27.690122+0800 MultithreadingDemo[41445:4562301] 执行任务2--<NSThread: 0x6000011f5900>{number = 3, name = (null)}
2019-12-31 11:14:27.690202+0800 MultithreadingDemo[41445:4562301] 执行任务3--<NSThread: 0x6000011f5900>{number = 3, name = (null)}
2019-12-31 11:14:27.690285+0800 MultithreadingDemo[41445:4562301] 执行任务4--<NSThread: 0x6000011f5900>{number = 3, name = (null)}

解释:

这个和面试题5相比是把串行队列换成了并发队列。

先打印任务1再打印任务5和前面是一样的。然后异步函数会开启子线程去执行block1任务块,block1中先打印任务2,然后通过同步函数向并发队列中添加block2任务块,并发队列不需要等前一个任务完成就可以安排下一个任务执行,所以block2可以立马执行打印任务3,最后再打印任务4