iOS面试备战-多线程

iOS面试中多线程绝对是最重要的知识点之一,它在平常开发中会被普遍使用,并且多线程是有不少区分度很高的题目可供考察的。这篇文章会梳理下多线程和GCD相关的概念和几个典型问题。由于GCD相关的API用OC看着更直管一些,因此这期实例就都用OC语言书写。

概念篇

在面对一些咱们常见的概念时,咱们常有种这个东西我熟,就认为本身理解了,其实这种程度是不够的。当咱们能够清晰准确的向别人描述一个东西,并能理解其官方定义的每一个用语的含义,才算是咱们熟悉理解了它。因此这里单独抽一节讲下多线程中的概念。面试

进程,线程,任务,队列

进程:资源分配的最小单位。在iOS中一个应用的启动就是开启了一个进程。 线程:CPU调度的最小单位。一个进程里会有多个线程。 你们能够思考下,进程和线程为何是从资源分配和CPU调度层面进行定义的。objective-c

任务:每次执行的一段代码,好比下载一张图片,触发一个网络请求。 队列:队列是用来组织任务的,一个队列包含多个任务。安全

GCD

GCD(Grand Central Dispatch)是异步执行任务的技术之一。开发者只须要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程执行该任务。这里的线程管理是由系统处理的,咱们没必要关心线程的建立销毁,这大大方便了咱们的开发效率。也能够说GCD是一种简化线程操做的多线程使用技术方案。markdown

安卓没有跟GCD彻底相同的一套技术方案的,虽然它能够处理GCD实现的一系列效果。网络

串行,并行,并发

GCD的使用都是经过调度队列(Dispatch Queue)的形式进行的,调度队列有如下 几种形式:多线程

串行(serial):多任务中某时刻只能有一个任务被运行;并发

并行(parallel):相对于串行,某时刻有多个任务同时被执行,须要多核能力;异步

并发(concurrent):引入时间片和抢占以后才有了并发的说法,某个时间片只有一个任务在执行,执行完时间片后进行资源抢占,到下一个任务去执行,即“微观串行,宏观并发”,因此这种状况下只有一个空闲的某核,多核空闲就又能够实现并行运行了;async

咱们经常使用的调度队列有如下几种:函数

// 串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.serialQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.gcd.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
// 全局并发队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 主队列
let mainQueue = DispatchQueue.main
复制代码

注意GCD建立的是并发队列而不是并行队列。但这里的并发队列是一个相对宽泛的定义,它包含并行的概念,GCD做为一个智能的中心调度系统会根据系统状况判断当前可否使用多核能力分摊多个任务,若是知足的话此时就是在并行的执行队列中的任务。

同步,异步

同步:函数会阻塞当前线程直到任务完成返回才能进行其它操做;

异步:在任务执行完成以前先将函数值返回,不会阻塞当前线程;

串行、并发和同步、异步相互结合可否开启新线程

串行队列 并发队列 主队列
同步 不开启新线程 不开启新线程 不开启新线程
异步 开启新线程 开启新线程 不开启新线程

主线程和主队列

主线程是一个线程,主队列是指主线程上的任务组织形式。

主队列只会在主线程执行,但主线程上执行的不必定就是主队列,还有多是别的同步队列。由于前说过,同步操做不会开辟新的线程,因此当你自定义一个同步的串行或者并行队列时都是还在主线程执行。

判断当前是不是主线程:

BOOL isMainThread = [NSThread isMainThread];
复制代码

判断当前是否在主队列上:

static void *mainQueueKey = "mainQueueKey";
dispatch_queue_set_specific(dispatch_get_main_queue(), mainQueueKey, &mainQueueKey, NULL);
BOOL isMainQueue = dispatch_get_specific(mainQueueKey));
复制代码

队列与线程的关系

队列是对任务的描述,它能够包含多个任务,这是应用层的一种描述。线程是系统级的调度单位,它是更底层的描述。一个队列(并行队列)的多个任务可能会被分配到多个线程执行。

问题

代码分析

一、分析下面代码的执行逻辑

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self syncMainTask];
}

- (void)syncMainTask {
    dispatch_queue_main_t mainQueue = dispatch_get_main_queue();
    dispatch_sync(mainQueue, ^{
        NSLog(@"main queue task");
    });
}
复制代码

这段代码会输出task1,而后发生死锁,致使crash。

追加问题一:为何会死锁?死锁就会致使crash?

咱们先分析crash的状况,正常死锁应该就是卡死的状况,不该该致使carsh。那为何会carsh呢,看崩溃信息:

是一个EXC_BAD_INSTRUCTION类型的crash,执行了一个出错的命令。

而后看__DISPATCH_WAIT_FOR_QUEUE__的调用栈信息:

右侧汇编代码给出了更详细的crash信息:BUG IN CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by current thread

在当前线程已经拥有的队列中执行dispatch_sync同步操做会致使crash。

libdispatch的源码中咱们能够找到该函数的定义:

DISPATCH_NOINLINE
static void
__DISPATCH_WAIT_FOR_QUEUE__(dispatch_sync_context_t dsc, dispatch_queue_t dq) {
    uint64_t dq_state = _dispatch_wait_prepare(dq);
    if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {
	DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,
			"dispatch_sync called on queue "
			"already owned by current thread");
    }
    /*...*/
}
复制代码

因此咱们知道了,这个carsh是libdispatch内部抛出的,当它检测到可能发生死锁时,就直接触发崩溃,事实上它不能彻底判断出全部死锁的状况。

咱们分析这里为何会发生死锁。首先syncMainTask就是在主队列中的,咱们在主队列先添加dispatch_sync而后再添加其内部的block。主队列FIFO,只有sync执行完了才会执行内部的block,而此时是一个同步队列,block执行完才会退出sync,因此致使了死锁。

对于死锁的解释我也查了好几篇文章,有些说法实际上是经不起推敲的,这个解释是我认为相对合理的。

附一篇参考文章:GCD死锁

引出问题二:什么状况下会发生死锁?

GCD中发生死锁须要知足两个条件:

  • 同步执行串行队列
  • 执行sync的队列和block所在队列为同一个队列

引出问题三:如何避免死锁?这段代码应该如何修改?

根据上面提到的条件,咱们能够将任务异步执行,或者换成一个并发队列。另外将block放到一个非主队列里执行也是能够的。

二、分析一下代码执行结果

int a = 0;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
while (a < 2) {
    dispatch_async(queue, ^{
        a++;
    });
}
NSLog(@"a = %d", a);
复制代码

首先该段代码会编译不过,编译器检测到变量a被block截获,并尝试修改就报如下错误:

Variable is not assignable (missing __block type specifier)。若是咱们要在block里对外界变量从新复制,须要添加__block的声明:__block int a = 0;

咱们分析这段代码,在开始while以后加入一个异步任务,再以后呢,这个是不肯定了,多是执行a++也多是因不知足退出条件再次执行加入异步任务,直到知足a<2才会退出while循环。那输出结果也就是不肯定了,由于可能在判断跳出循环和输出结果的时候另外的线程又执行了一次a++

再扩展下,若是将那个并发队列改为主队列,执行逻辑仍是同样的吗?

首先主队列是不会开启新线程的,主队列上的异步操做执行时机是等别的任务都执行完了,再来执行添加的a++。显然在while循环里,主队列既有任务还未执行完毕,因此就不会执行a++,也就致使while循环不会退出,造成死循环。

其它问题

什么是线程安全,为何UI操做必须在主线程执行

线程安全:当多个线程访问某个方法时,无论你经过怎样的调用方式或者说这些线程如何交替的执行,咱们在主程序中不须要去作任何的同步,这个类的结果行为都是咱们设想的正确行为,那么咱们就能够说这个类时线程安全的。

为何UI操做必须放到主线程:首先UIKit不是线程安全的,多线程访问会致使UI效果不可预期,因此咱们不能使用多个线程去处理UI。那既然要单线程处理UI为何是在主线程呢,这是由于UIApplication做为程序的起点是在主线程初始化的,因此咱们后续的UI操做也都要放到主线程处理。

关于这个问题展开讨论能够参阅这篇文章:iOS拾遗——为何必须在主线程操做UI

开启新的线程有哪些方法

一、NSThread

二、NSOperationQueue

三、GCD

四、NSObject的performSelectorInBackground方法

五、pthread

多线程任务要实现顺序执行有哪些方法

一、dispatch_group

二、dispatch_barrier

三、dispatch_semaphore_t

四、NSOperation的addDependency方法

如何实现一个多读单写的功能?

多读单写的意思就是能够有多个线程同时参与读取数据,可是写数据时不能有读操做的参与切只有一个线程在写数据。

咱们写一个示例程序,看下在不作限制的多读多写程序中会发生什么。

// 计数器
self.count = 0;
// 并发队列
self.concurrentQueue = dispatch_get_global_queue(0, 0);
for (int i = 0; i< 10; i++) {
    dispatch_async(self.concurrentQueue, ^{
        [self read];
    });
    dispatch_async(self.concurrentQueue, ^{
        [self write];
    });
}
// 读写操做
- (void)read {
    NSLog(@"read---- %d", self.count);
}

- (void)write {
    self.count += 1;
    NSLog(@"write---- %d", self.count);
}

// 输出内容
2020-07-18 11:47:03.612175+0800 GCD_OC[76121:1709312] read---- 0
2020-07-18 11:47:03.612273+0800 GCD_OC[76121:1709311] read---- 1
2020-07-18 11:47:03.612230+0800 GCD_OC[76121:1709314] write---- 1
2020-07-18 11:47:03.612866+0800 GCD_OC[76121:1709312] write---- 2
2020-07-18 11:47:03.612986+0800 GCD_OC[76121:1709311] write---- 3
2020-07-18 11:47:03.612919+0800 GCD_OC[76121:1709314] read---- 2
2020-07-18 11:47:03.613252+0800 GCD_OC[76121:1709312] read---- 3
2020-07-18 11:47:03.613346+0800 GCD_OC[76121:1709314] write---- 4
2020-07-18 11:47:03.613423+0800 GCD_OC[76121:1709311] read---- 4
复制代码

每次运行的输出结果都会不同,根据这个输出内容,咱们能够看到在尚未执行到输出write----1的时候,就已经执行了read----1,在write---- 3以后 read的结果倒是2。这绝对是咱们所不指望的。其实在程序设计中咱们是不该该设计出多读多写这种行为,由于这个结果是不可控。

解决方案之一是对读写操做都加上锁作成单独单写,这样是没问题但有些浪费性能,正常写操做肯定以后结果就肯定了,读的操做能够多线程同时进行,而不须要等别的线程读完它才能读,因此有了多读单写的需求。

解决多读单写常见有两种方案,第一种是使用读写锁pthread_rwlock_t

读写锁具备一些几个特性:

  • 同一时间,只能有一个线程进行写的操做
  • 同一时间,容许有多个线程进行读的操做。
  • 同一时间,不容许既有写的操做,又有读的操做。

这跟咱们的多读单写需求完美吻合,也能够说读写锁的设计就是为了实现这一需求的。它的实现方式以下:

// 执行读写操做以前须要定义一个读写锁
@property (nonatomic,assign) pthread_rwlock_t lock;
pthread_rwlock_init(&_lock,NULL);
// 读写操做
- (void)read {
    pthread_rwlock_rdlock(&_lock);
    NSLog(@"read---- %d", self.count);
    pthread_rwlock_unlock(&_lock);
}

- (void)write {
    pthread_rwlock_wrlock(&_lock);
    _count += 1;
    NSLog(@"write---- %d", self.count);
    pthread_rwlock_unlock(&_lock);
}
// 输出内容
2020-07-18 12:00:29.363875+0800 GCD_OC[77172:1722472] read---- 0
2020-07-18 12:00:29.363875+0800 GCD_OC[77172:1722471] read---- 0
2020-07-18 12:00:29.364195+0800 GCD_OC[77172:1722469] write---- 1
2020-07-18 12:00:29.364325+0800 GCD_OC[77172:1722472] write---- 2
2020-07-18 12:00:29.364450+0800 GCD_OC[77172:1722470] read---- 2
2020-07-18 12:00:29.364597+0800 GCD_OC[77172:1722471] write---- 3
2020-07-18 12:00:29.366490+0800 GCD_OC[77172:1722469] read---- 3
2020-07-18 12:00:29.366703+0800 GCD_OC[77172:1722472] write---- 4
2020-07-18 12:00:29.366892+0800 GCD_OC[77172:1722489] read---- 4
复制代码

咱们查看输出日志,因此的读操做结果都是最近一次写操做所赋的值,这是符合咱们预期的。

还有一种实现多读单写的方案是使用GCD中的栅栏函数dispatch_barrier。栅栏函数的目的就是保证在同一队列中它以前的操做所有执行完毕再执行后面的操做。为了保证写操做的互斥行,咱们要对写操做执行「栅栏」:

// 咱们定义一个用于读写的并发对列
self.rwQueue = dispatch_queue_create("com.rw.queue", DISPATCH_QUEUE_CONCURRENT);

- (void)read {
    dispatch_sync(self.rwQueue, ^{
        NSLog(@"read---- %d", self.count);
    });
}

- (void)write {
    dispatch_barrier_async(self.rwQueue, ^{
        self.count += 1;
        NSLog(@"write---- %d", self.count);
    });
}
复制代码

这个输出结果跟读写锁实现是同样的,也是符合预期的。

这里多说几句,这里的读和写分别使用syncasync。读操做要用同步是为了阻塞线程尽快返回结果,不用担忧没法实现多读,由于咱们使用了并发队列,是能够实现多读的。至于写操做使用异步的栅栏函数,是为了写时不阻塞线程,经过栅栏函数实现单写。若是咱们将读写都改为sync或者async,因为栅栏函数的机制是会顺序先读后写。若是反过来,读操做异步,写操做同步也是能够达到多读单写的目的的,但读的时候不当即返回结果,网上有人说只能使用异步方式,防止发生死锁,这个说法其实不对,由于同步队列是不会发生死锁的。

用GCD如何实现一个控制最大并发数且执行任务FIFO的功能?

这个相对简单,经过信号量实现并发数的控制,经过并发队列实现任务的FIFO的执行

int maxConcurrent = 3;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(maxConcurrent);
dispatch_async(queue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // task
    dispatch_semaphore_signal(semaphore);
});
复制代码

相关文章
相关标签/搜索