iOS多线程整理

iOS多线程整理


知识点梳理

1.线程进程的区别:

> 进程:应用程序的实例
> 线程:任务调度的基本单元

2.队列种类:

串行队列、并发队列、主队列(有经过特殊处理的串行队列)、全局队列(属于并发队列)

> 串行队列:队列中的任务按顺序一个一个执行,任务的执行必须有先后顺序
> 并发队列:具有并发执行队列中任务的能力
> 主队列:绑定主线程,所有任务都在主线程中执行
> 全局队列:系统提供的并发队列

串行并行的区别:

串行:表示在某个时刻只有一个任务在执行
并行:表示在某个时刻有多个任务在执行

3.并发与并行的区别:

并发 Concurrency [kən'kʌrənsɪ]:可以同时接受多个任务,使多个任务得到处理的特性

1.真实的情况。比如:一个程序猿可以揽10个需求同时去做。一个程序猿在做需求期间可抽空学习或接私活。一个工厂可以接10个订单同时去生产。单核CPU可同时处理多个应用程序。

2.比如并发队列。并发队列能够处理多个任务,使多个任务不用彼此等待同时得到处理。(扩展:并发队列如何实现并发特性?通过开辟多个子线程去处理这多个任务,以此来实现并发特性)

3.比如单核CPU实现并发(扩展:单核CPU如何实现并发?通过时间片轮转调度)

并行 parallel [ˈpærəˌlɛl]:某个时刻多个任务能够同时执行的能力

1.真实的情况。比如:人可以让两只手一起握拳(人可并行握拳),一个打了10个孔的水管可以同时浇10盆花(打孔水管可并行浇花),等等...

2.比如多核CPU的并行计算,同一时刻CPU的每个核心可以单独执行指令,上面说到的单核CPU是没有这个能力的。

其他理解:

1.系统中有多个任务同时存在可称之为“并发”,系统内有多个任务同时执行可称之为“并行”

2.并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生

举例:工厂加工糖果

不具有并发特性的工厂,无并行能力:工厂每次只能接一个订单,多的订单往后排,一个做完再做下一个;只有一台机器生产糖果

具有并发特性的工厂,无并行能力:工厂可以一次性接多个订单;只有一台机器交替生产这多个订单的糖果

不具有并发特性的工厂,具有并行能力:工厂每次只能接一个订单,多的订单往后排,一个做完再做下一个;有多台机器一起生产这一个订单的糖果

具有并发特性的工厂,具有并行能力:工厂可以一次性接多个订单;有多台机器一起生产这多个订单的糖果

举例:CPU执行任务

单核cpu非并发执行任务:单核CPU一次处理一个完整任务

单核cpu并发执行任务:单核CPU交替处理多个任务,每次只处理某个任务的一部分

多核cpu非并发执行任务:多核CPU一次处理一个任务,将任务拆分成多个子任务,多个核心同时单独的执行这些子任务

多核cpu并发执行任务:多核CPU一次处理多个任务,将任务拆分,多个核心同时单独的执行这些子任务

问题:

既然串行和并行是反义词,为什么都说并发队列,而不说并行队列:计算机硬件和系统可能并非能真正的并行执行任务。比如单核cpu,也可以实现并发,但是不具有并行能力。

4.操作:

> 同步:synchronize[ˈsɪŋkrənəs],同步任务需要使当前任务等待
> 异步:asynchronous[e'sɪŋkrənəs],异步任务无需使当前任务等待

同步异步的理解:

我们写的的代码其实是被包裹在一个任务中的,这个任务在队列中排队,然后轮到它时就在队列绑定的线程中执行。

如下,整块代码也都是被包裹在一个任务中,这个任务在主队列排队,然后最后轮到它时放到主线程执行。

// 任务1最开始....
...
// 以下的代码也只是任务1中的一个片段
// 主线程环境中
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 此处大括号包裹的整个是任务2
    // do something in task2
});
...
// 任务1最末尾...

等同于:

// 任务1最开始....
...
// 以下的代码也只是任务中的一个片段
// 主线程环境中
// do something in task1
...
// 任务1最末尾...

等价设定:

“当前线程执行的任务” <=> “任务1”
“需要执行的任务”:<=> “任务2”
1.同步:任务2与任务1同步,任务1要等待任务2执行完毕后才能继续执行(扩展猜测:由于是同步,任务1要等待任务2,所以此时开新线程执行任务2和不开新线程执行任务2,从期望的结果来看没什么区别,则直接在当前线程中执行任务2即可)

2.异步:任务2与任务1异步,任务1不用等待任务2完成就可继续执行

总结:同步异步是针对多线程代码和当前所在环境之间的关系,用来控制“当前线程执行的任务”是否要等待“需要执行的任务”,与队列无关,与队列中的其他任务无关。

5.iOS中的多线程规则

情况是否新开线程与当前执行代码所属任务的关系
串行队列同步当前线程执行当前任务需等待
串行队列异步新开线程执行(每个任务都在同一个线程执行)当前任务无需等待
并发队列同步当前线程执行当前任务需等待
并发队列异步新开线程执行当前任务无需等待

6.扩展知识:执行栈

1.常被用于存放子程序的返回地址

2.在调用任何子程序时,主程序都必须暂存子程序运行完毕后应该返回到的地址

3.如果被调用的子程序还要调用其他的子程序,其自身的返回地址就必须存入执行栈,在其自身运行完毕后再行取回

4.在递归程序中,每一层次递归都必须在执行栈上增加一条地址,因此如果程序出现无限递归(或仅仅是过多的递归层次),执行栈就会产生栈溢出。

比如:

void main() {
    int i = 0
    aMethod()
    bMethod()
}
void aMethod {
}
void bMethod {
    cMethod()
}
void cMethod {
}

堆栈过程:

null
main
main - aMethod
main
main - bMethod
main - bMethod - cMethod
main - bMethod
main
null

一些问题的理解

问题一:主线程环境中,在主队列上执行同步任务,为什么会死锁

1.假设:假设当前执行的代码是包含在任务1中,在主队列上执行的同步任务为任务2。

// 任务1
// 主线程环境中
dispatch_sync(dispatch_get_main_queue(), ^{
    // 任务2
    // do something in task2
});

2.同步角度思考:由于是是同步任务,所以任务1此时需要等待任务2执行,任务2执行完毕后任务1才能继续执行下去。

3.队列角度思考:任务2会被加到主队列的队尾,由于串行队列的特性,任务必须一个一个执行。因此任务2需要等待队列中其他任务(包括任务1)都执行完之后才会轮到它去执行。

4.结果:所以出现了任务2等待任务1,任务1等待任务2的情况,导致死锁。
此外如果串行队列绑定线程a,那么在线程a环境中,在该串行队列上执行同步任务,也会导致死锁。原因同上。

问题二:主线程环境中,为什么在新创建的串行队列中执行同步任务就不会死锁

1.假设:假设当前执行的代码是包含在任务1中,在串行队列上执行的同步任务为任务2。

// 任务1
// 主线程环境中
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 任务2
    // do something in task2
});

2.同步角度思考:由于是是同步任务,所以任务1此时需要等待任务2执行,任务2执行完毕后任务1才能继续执行下去。

3.队列角度思考:任务2会被加到串行队列zcp的队尾,任务2只跟队列zcp中的其他任务有先后顺序关系,跟其他队列上的任务无关,也就是说任务2跟主队列中的其他任务无关,所以任务2不会等待任务1

4.结果:任务1等待任务2,任务2不用等待任务1,任务2执行完毕后,然后继续执行任务1。

API介绍

详细内容可参考:iOS多线程-归纳与总结

1.NSThread

管理多线程困难,推荐使用NSOperation和GCD。

应用场景:

> 1.使用[NSThread currentThread]获取当前线程
> 2.使用[NSThread mainThread]获取主线程

2.NSOperation

GCD的封装,代码风格更OC。

特点:

1.可以控制暂停、恢复、停止。suspended、cancel、cancelAllOperations

2.可以控制任务的优先级。threadPriority和queuePriority

3.可以设置依赖关系。addDependency和removeDependency

4.可以控制并发个数。maxConcurrentOperationCount

5.NSOperation有两个封装的便利子类NSBlockOperation、NSInvocationOperation,他们都使用了并发队列

队列的种类:

主队列 [NSOperationQueue mainQueue],是串行队列

非主队列 [NSOperationQueue new],是并发队列

NSOperation的执行过程:

当operation加入到queue中时,会在相关线程中执行operation的start方法,main方法在start方法中调用。

线程判定:

根据queue来决定在哪个线程中执行start方法。

[NSOperationQueue mainQueue]:在主线程中执行

[NSOperationQueue currentQueue]:在当前线程中执行

[NSOperationQueue new]:新开线程执行,该队列为并发队列

start方法和main方法的执行顺序:

start方法内部做了一些有关安全的逻辑判断,判断结束后执行main。

因此如果自己写了一个类继承自NSOperation,重写start方法时要注意,main方法会在[super start]中执行,如果不调用[super start]则main方法不执行,另外要注意[super start]与前后代码的执行顺序。

应用场景:

可以参考AFNetworking2.x版本中的AFURLConnectionOperation类和AFHTTPRequestOperation

3.GCD

1.队列与操作

队列与操作
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_SERIAL)
// 并发队列
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_CONCURRENT)
// 主队列
dispatch_queue_t queue = dispatch_get_main_queue()
// 全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
// 同步操作
dispatch_sync(queue, ^{
})
// 异步操作
dispatch_async(queue, ^{
})
其他功能
// 暂停队列
dispatch_suspend(dispatch_object_t object);
// 恢复队列
dispatch_resume(dispatch_object_t object);

2.其他内容

dispatch_after
// 延迟5秒执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
dispatch_once

确保程序执行过程中只被执行一次,且线程安全,常用于单例。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
任务组

用来处理多个任务都完成后再执行的动作

// 队列,可以根据情况使用合适的queue
dispatch_queue_t queue = dispatch_queue_create("zcp", DISPATCH_QUEUE_CONCURRENT);
// 创建任务组
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    // 任务1
});
dispatch_group_async(group, queue, ^{
    // 任务2
});
dispatch_group_async(group, queue, ^{
    // 任务3
});
dispatch_group_notify(group, queue, ^{
    // 任务1、任务2、任务3都执行完毕之后才会执行这里
});


// 系统管理队列组:
dispatch_group_async(group, queue, ^{
    // do something
});
// 等价于
// 手动管理队列组:
dispatch_group_enter(group);
dispatch_async(queue, ^{
    // do something
    dispatch_group_leave(group);
});
dispatch_semaphore
// 创建信号量
dispatch_semaphore_create
// 信号量-1
dispatch_semaphore_wait
// 信号量+1
dispatch_semaphore_signal
// YYCache中的YYDiskCache类中
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
dispatch_barrier

参见:iOS多线程编程总结

dispatch_barrier_async:
dispatch_async(queue, block1_for_reading)  
dispatch_async(queue, block2_for_reading)

dispatch_barrier_async(queue, block_for_writing)

dispatch_async(queue, block3_for_reading)  
dispatch_async(queue, block4_for_reading)  

/*
dispatch_barrier_async会把并行队列的运行周期分为这三个过程:

首先等目前追加到并行队列中所有任务都执行完成
开始执行dispatch_barrier_async中的任务,这时候即使向并行队列提交任务,也不会执行
dispatch_barrier_async中的任务执行完成后,并行队列恢复正常。

这样一来,使用并行队列和dispatc_barrier_async方法,就可以高效的进行数据和文件读写了。
*/

4.其他问题

多线程与runloop的关系:

每个线程都有一个runloop,主线程默认开启,子线程默认休眠。
一般来讲,一个线程一次只能执行一个任务,执行完毕后线程就会退出,开启runloop可以让线程能随时处理事件但并不退出

多线程不安全的情况:
__block int a = 0;
for (int i = 0; i < 100; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"%d", a++);
    });
}

// 结果并不为100

原因:

a++这句代码其实相当于a = a+1,由于代码是在并发队列中异步执行的,所以相当于有100个a = a+1同时执行。

a++代码的执行过程如下:

> 1.取到a的值
> 2.计算a+1的值
> 3.将计算结果赋值给a

正常情况:

第1次:执行a++,取到a为0,计算a+1结果为1,将1赋值给a。

异常情况:

第1次:执行a++,取到a为0,
第10次:执行a++,取到a为0,计算a+1结果为1,将1赋值给a
第56次:执行a++,取到a为1,计算a+1结果为2,将2赋值给a
第27次:执行a++,取到a为2,计算a+1结果为3,将3赋值给a
第1次:计算a+1结果为1,将1赋值给a

以上是一种异常情况的假设,实际的执行情况会更复杂。在第一次取到a为0时,其他线程已经跑了很多句a++的代码使a变成了3,这个时候才开始计算第一次的a+1,a又变成了1。导致前面几次的计算都没意义了。
引用时间片轮转调度中的一段话:

在自己的程序运行时不是独一无二的,我们看似很顺畅的工作,其实是由一个个的执行片段构成的,我们眼中相邻的两条语句甚至同一个语句中两个不同的运算符之间,都有可能插入其他线程或进程的动作。


使用案例

1.处理耗时任务

本地持久化:如果在主线程中存储数据,数据量比较大时会阻塞主线程造成页面卡顿。需要新开线程在后台处理。另外还有使用dispatch_barrier_async和CoreData的案例。

耗时代码处理:如果使用多次数的循环语句,或者是使用非常耗时的api时,会影响到主线程导致卡顿。可以新开线程在后台处理,然后如果有需要刷新UI则在主线程中同步。

2.网络请求等待

接口请求:接口请求受网络环境影响,是不可能在主线程请求并等待的。需要新开线程异步请求。如使用NSURLSession的dataTaskWithURL:方法(或NSURLConnection的sendAsynchronousRequest:方法)异步请求;如AFNetworking中异步请求代码。

加载网络资源:加载网络中的大图或下载文件会很耗时,需要在后台线程加载。如在子线程中使用NSData的dataWithContentsOfURL:下载文件;如SDWebImage的异步下载。

3.其他情况

任务组:常会遇到某个逻辑判断需要两个接口中的数据,比如当获取业务线和列表数据之后才渲染页面。就可以用任务组。

延迟调用:比如一些动画的实现需要延时。


后续

iOS中的锁


参考文章

iOS多线程-归纳与总结

iOS多线程编程总结

GCD 深入理解:第一部分


相关推荐