iOS 并发概念浅析
在进行iOS开发过程中,我们常会遇到网络请求、复杂计算、数据存取等比较耗时的操作,如果处理不合理,将对APP的流畅度产生较大影响。除了优化APP架构,并发(concurrency)是一个常用且较好的解决方法,但并发涉及串行、并发、并行、同步、异步、多线程、GCD、NSOperation和NSOperationQueue等诸多容易混淆的概念,为求概念清晰明了,还请茗茶静坐,听我徐徐道来。
一、线程和任务
线程(thread) 和任务(task)是其他并发概念的基础,因此也是首要需理清的概念,以下是其要点,详细可参考Thread (computing)和Task (computing))。
1)任务(task)
a)任务(task)是从程序中划分出来,可以独立执行的代码片段;
b)任务间可以添加依赖关系,如B任务依赖A任务,taskB.addDependency(taskA),这意味着B任务的执行以A任务完成为前提。
需要注意的是一个任务是否可以添加依赖,完全取决于任务封装类和其相关管理类的具体实现,GCD不支持任务依赖,NSOperationQueue就支持任务依赖。
下面的代码是对一个任务的简单封装,并支持任务间的依赖。
//Task是一个任务的简单封装类
class Task {
let taskBlock: () -> ()
var dependencies = [Task]()
init(block: () -> ()) {
taskBlock = block
}
func addDependency(task: Task) {
dependencies.append(task)
}
}
//初始化两个自定义任务
var taskA = Task(){
//自定义任务A,自定义需要执行的代码
...
}
var taskB = Task(){
//自定义任务B,自定义需要执行的代码
...
}
//添加依赖关系
taskB.addDependency(taskA)
2)线程(thread)
a)线程(thread)是代码执行的独立路径,一条线程只能同时执行一行代码(一行代码,其实就是一条处理器命令)。
b)线程中代码管理是以任务(task)为单位,一条线程逐行执行一个任务中的代码(任务可以取消),完成后再逐行执行下一个任务中的代码。
c)一条线程跳出一个任务的执行,即意味着这个任务的完成。因此,一条线程不能执行taskA一段时间后,还未完成就开始执行taskB,然后又返回执行taskA(这其实是单线程内的并发,与单核处理器的并发概念相同,具体实践中不存在线程内并发)。
二、概念释疑
1)并行(parallelism)和并发(concurrency)
并发和并行都是指多个任务可以同时执行,都属于多线程编程概念,因此二者必然十分相近,容易混淆。二者区别只有一点,即是否多任务执行于严格的同一时刻。并发不是,并行是。
单核处理器时代(一个处理器同一时刻只能执行一条命令),为了实现多任务的同时执行,系统利用时间分片(time-slicing)技术,将处理器的执行时间切分为多个小片段,一会执行threadA,一会执行threadB,一会再执行threadA,即在多个线程(任务是在线程上执行的)之间来回跳动执行。虽不是真的多线程多任务同时执行,但由于处理器的处理速度非常快,在用户看来,仍然是同时执行的。这种伪多线程就是并发。
多核处理器时代(不同处理器相互独立,可以同时执行各自的命令),多条线程完全可以严格同一时刻执行,这种真多线程就是并行。
//三个线程的并发
thread1 -> |---A---| ->|---A---|
\ / \
thread2 ------> ->|------B----| \
thread3 ------------------------------------> |------C------|
上述代码是三个线程的并发执行,可以看出thread1、thread2和thread3不可能严格同一时刻执行,但也都获得了处理器的一小段执行时间。
//三个线程的并行
thread1 -> |-----A-----|
thread2 -> |------B----|
thread3 -> |------C------|
上述代码是三个线程的并行执行,可以看出thread1、thread2和thread3有一段时间同时执行。
现在的终端设备无论是手机还是PC的处理器,大多都已是多核处理器,可以实现并行计算,但为了最大化的利用处理器的性能,现代处理器还是融合了time-slicing技术和多核技术,因此实际运行中,有时并发,有时并行。但相对来说,并发是个更广泛的概念,因此Apple的多线程编程叫做concurrency programming并发编程。汉语中,并发和并行的区别其实没那么清晰,可以互用,而且有时用并行更加明确,如串并行比串行、并发针对性更强。(为概念清晰期间,下文中有时会用并行,其实即是并发。)
2)串并行与线程
串行(serial)和并行
串行和并行主要区别在于一个任务的执行是否以上一个任务的完成为前提。串行中,一个任务的执行必须以上一个任务执行结束为前提,并行中,一个任务的执行与上一个任务的执行状态无关。以排队买票为例,串行像单个买票队伍,单个卖票窗口,必须一个一个来,串行像单个买票队伍,多个卖票窗口,多个人可以同时买票。
//三个串行任务
|-----A-----||------B--------||----C--|
上文为三个串行任务,任务A完成后,才执行任务B,B结束后,才最后执行任务C。
//三个并发任务
|-----A-----|
|------B----|
|--C---|
上文为三个并行任务,任务A早于任务C开始,却晚于任务C结束。
串并行与线程
串并行主要关注多个任务之间的相互依赖关系,与线程无关。但实际中,任务是在线程中执行的,是否串行一定在单线程上执行,并行一定在多个线程中执行呢?并非如此。
单线程既可以实现串行,也可以实现并行。
//单线程串行
1 thread -> |----A-----||-----B-----------||-------C------|
//单线程并行(理论上,实际中不可行)
A-Start ---------------------------------------- A-End
| B-Start ----------------------------------------|--- B-End
| | C-Start -------------------- C-End | |
V V V V V V
1 thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
需要指出的是单线程内的并行已经类似单核处理器,并不是本文提及的常规线程,现实中也不常见。
多线程既可以实现串行,也可以实现并行,实际上,多线程串行和并行都很常见。
//多线程串行
thread1 -> |----A-----|
\
thread2 --------------->|-----B-----------|
\
thread3 ----------------------------------->|-------C------|
//多线程并发
thread1 -> |----A-----|
thread2 -----> |-----B-----------|
thread3 ---------> |-------C----------|
3)同步(synchronize)、异步(asynchronous)与线程
同步和异步是站在当前线程的角度,考察添加任务到新线程后,何时返回到当前线程执行下面的代码的问题,也即新添加的线程阻不阻塞当前线程。
同步
override viewDidLoad() {
super.viewDidLoad()
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_sync(queue) {
//block1
print("-----1-----") //1
return
}
print("-----2-----") //2
}
block1是添加到系统全局队列中的新任务,由于是同步的,因此block1执行返回后,才会回到当前主线程,执行//2及以后的代码。输出结果为:
-----1-----
-----2-----
异步
//viewDidLoad()在主线程中执行,因此当前线程为主线程
override viewDidLoad() {
super.viewDidLoad()
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(queue) {
//block1
print("-----1-----") //1
return
}
print("-----2-----") //2
}
block1是添加到系统全局队列中的新任务,由于是异步的,因此block1添加全局队列后(会在另外一个线程上执行),不等到执行完成,就会返回到当前主线程,执行//2及以后的代码,所以输出结果可能为 21 12。但由于block1和主线程中的任务都是不耗时的简单任务,而创建新的线程是要消耗一定时间的(主线程一直存在,不用新创建),因此很可能的输出结果是:
-----2-----
-----1-----
同异步结合的情形
如果同异步结合:
//viewDidLoad()在主线程中执行,因此当前线程为主线程
override viewDidLoad() {
super.viewDidLoad()
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_async(queue) {
//block1
print("-----A-----") //1
dispatch_async(dispatch_get_main_queue()) {
//block2
print("-----B-----") //2
}
print("-----C-----") //3
return
}
print("-----D-----") //4
while(true) { } //5
print("-----E-----") //6
}
block1是添加到系统全局队列中的新任务,由于是异步的,因此block1添加全局队列后(会在另外一个线程上执行),不等到执行完成,就返回到当前主线程,执行//4及以后的代码,结果是block1所在的线程与主线程同时执行,因此理论上,D和A谁先输出不一定。但由于block1和主线程中的任务都是不耗时的简单任务,而创建新的线程是要消耗一定时间的(主线程一直存在,不用新创建),因此一般输出结果为DA。
block1所在线程输出完A后,将block2添加到主调度队列中,由于是异步的,因此block2添加主调度队列后(会在主线程上执行),不等到执行完成,就返回到block2所在的线程,继续执行,因此A和C一定会输出,且C一定在A之后输出。但block2却不一定能执行,因为block1在执行时,主线程也在执行(主线程是串行单线程,任务按顺序一个一个执行),如果此时主线程执行到//5对应的死循环,则block2一定不能被执行,B一定不能被输出,如果此时主线程尚未执行到//5对应的死循环,block2已经添加到主线程中,则block2会被执行,B能被输出。但由于主线程无需另外创建,block1(所对应的线程需另外创建)执行到添加block2到主调度队列时,主线程很可能已经执行到//5对应的死循环,因此block2很可能不被执行。
//6前有个死循环,因此E一定不会被输出。
因此可能的输出结果是;DAC ADC ADCB DACB ACDB ACBD ABDC ABCD
但很可能的输出结果为:
-----D-----
-----A-----
-----C-----
4)同异步与串并行
串行和同步,并行和异步似是完全不同的概念,一个关注任务的独立关系,一个看中的是返回的时机。但事实上,串行和同步近似,并发和异步相同,他们指代的事情几乎完全相同。
就同步和串行而言,需要任务执行结束后才能返回,其实就是一个任务执行完成后,才能执行其他的任务,反应的就是串行依赖关系。
而异步和并行就更相同了,不等任务执行完成,就直接返回,反应的就是并发任务之间的独立性。
当然,同异步所暗含的串行和并行是当前线程的任务与新线程的任务之间的相互关系。
三、GCD与NSOperationQueue
GCD(grand central dispatch)和NSOperationQueue二者均是系统级的多线程封装,在使用时,我们只需创建任务队列即可,其他的如线程创立、任务分配等,均由系统自动处理。不得不说,这让多线程编程变得更高效,更简单,当然并不是没有坑。
需要强调的是,GCD和NSOperationQueue的使用核心是任务(task)和任务队列(task queue),暂时可以忘了线程(thread)这烦人的概念。
关于GCD和NSOperationQueue网上已经有不少高质量的文章对其详细介绍,我推荐《iOS并行开发:从NSOperation和调度队列开始》,其对基本概念、使用方法等的介绍非常清晰详尽,我这里就不再赘述了,只写一些我认为容易忽略却影响认知深度的小知识点。当然如果你英语过硬,去直接看官方文档《ConcurrencyProgrammingGuide》是最好的。
1)GCD
GCD是基于C的API,因此比较底层。
GCD所管理的调度队列(dispatch queue)主要有三类,串行队列(private dispatch queue)、并发队列 (global dispatch queue,又称全局调度队列)和主队列(main dispatch queue)。
我们常用的 dispatch_get_global_queue(_:_:)所获得的dispatch queue就是全局调度队列(global dispatch queue),并发,而且全局调度队列是全局共用的,每一个优先级的全局调度队列只有一个实体。四种不同优先级的全局调度队列对应的四种优先级的线程,同一个优先级的全局调度队列可以同时拥有多条相应优先级的线程。
dispatch_get_main_queue()所获得的dispatch queue是主调度队列,主调度队列是串行队列。
2)NSOperationQueue
NSOperationQueue是对GCD的Objective-C封装,相对于GCD具有更多先进的特性,如可以添加NSOperation依赖,取消NSOperation等。
NSOperationQueue是并发队列,且不遵循先进先出FIFO排序原则。
四、总结与感悟
上文基本就是我对并发的认识,有几个点我想在这里再强调下,
1)串并行、同异步与线程无关,单线程、多线程都可以实现串并行和同异步。
2)串行和同步相同,异步和并行相同,他们只是看待同一件事物的角度不同。
3)GCD和NSOperationQueue的使用核心是任务(task)和任务队列(task queue)。
4)全局调度队列(global dispatch queue)是全局共用的,系统有时也会向这些调度队列添加系统任务。
5) App的主调度队列是串行单线程队列。
通过本文的浅析,希望达到了理清概念的最初目的,当然这些只是并发编程的第一步,并发线程还有竟态资源、死锁等诸多大坑,要想真正掌握,我们继续努力。
iOS 并发概念浅析的更多相关文章
- iOS 并发编程之 Operation Queues
现如今移动设备也早已经进入了多核心 CPU 时代,并且随着时间的推移,CPU 的核心数只会增加不会减少.而作为软件开发者,我们需要做的就是尽可能地提高应用的并发性,来充分利用这些多核心 CPU 的性能 ...
- iOS 并发:NSOperation 与调度队列入门(1)
一直以来,并发都被视为 iOS 开发中的「洪水猛兽」.许多开发者都将其视为危险地带,唯恐避之而不及.更有谣传认为,多线程代码应该尽力避免.笔者同意,如果你对并发的了解不够深入,就容易造成危险.但是,危 ...
- 高并发第二弹:并发概念及内存模型(JMM)
高并发第二弹:并发概念及内存模型(JMM) 感谢 : 深入Java内存模型 http://www.importnew.com/10589.html, cpu缓存一致性 https://www.cnbl ...
- Python进阶----计算机基础知识(操作系统多道技术),进程概念, 并发概念,并行概念,多进程实现
Python进阶----计算机基础知识(操作系统多道技术),进程概念, 并发概念,并行概念,多进程实现 一丶进程基础知识 什么是程序: 程序就是一堆文件 什么是进程: 进程就是一个正在 ...
- IOS并发编程GCD
iOS有三种多线程编程的技术 (一)NSThread (二)Cocoa NSOperation (三)GCD(全称:Grand Central Dispatch) 这三种编程方式从上到下,抽象度层次 ...
- iOS 并发编程指南
iOS Concurrency Programming Guide iOS 和 Mac OS 传统的并发编程模型是线程,不过线程模型伸缩性不强,而且编写正确的线程代码也不容易.Mac OS 和 iOS ...
- iOS底层框架浅析
1.简介 IOS是由苹果公司为iPhone.iPod touch和iPad等设备开发的操作系统. 2.知识点 iPhone OS(现在叫iOS)是iPhone, iPod touch 和 iPad 设 ...
- iOS并发编程笔记【转】
线程 使用Instruments的CPU strategy view查看代码如何在多核CPU中执行.创建线程可以使用POSIX 线程API,或者NSThread(封装POSIX 线程API).下面是并 ...
- iOS 优化方案浅析
本文转载至 http://mobile.51cto.com/iphone-413256.htm Windows独特的注册表机制以及复杂的进程.内存管理,给了很多PC“优化”类软件极大的机遇,比如奇虎3 ...
随机推荐
- CSS3实现jquery的特效
实现 “慕课网” 的图片滑过缩放的效果 技术点:css3—— -webkit-transform:scale(1.2); .course-list-img .img_1:hover{ -webki ...
- (转载)memcpy的几个实现版本
(转载)http://blog.sina.com.cn/s/blog_4d3a41f40100cvza.html 实现void *memcpy(void *to, const void *from, ...
- java jvm学习笔记七(jar包的代码认证和签名)
欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 前言: 如果你循序渐进的看到这里,那么说明你的毅力提高了,jvm的很多东西都是比较抽像的,如果不找相对应的代码来辅助理解 ...
- extjs分组查询
<script type="text/jscript"> var grid; Ext.onReady(function () { Ext.QuickTips.init( ...
- WebDriver打开浏览器-java
环境:配置jdk.使用Eclipse(个人爱好),导入selenium-java-2.42.2.jar.selenium-java-2.42.2-srcs.jar.selenium-server-st ...
- DNS(一)简介
最近学习相关DNS知识,顺便总结下相关内容. 1.什么是DNS DNS(Domain Name System)服务,可以使用域名代替复杂的IP地址来访问网络服务器,使得网络服务的访问更加简单,而且可以 ...
- JDBCTemplate.java
package com.pk.xjgs.util; import java.sql.Connection; import java.sql.SQLException; import java.util ...
- ListView自定义适配器--10.17
1. 添加button 2. ViewHolder 优化性能 就是一个持有者的类,他里面一般没有方法,只有属性,作用就是一个临时的储存器,把你getView方法中每次返回的View存起来,可以下次再用 ...
- EntityFramework更新多条数据【8万】
此文主要用做记录用: 原因:数据库迁移,需要转换大量用户资料,两数据某字段加密方式不一致需要批量转换 注:转换程序用了EntityFramework 过程: 1.读取所有需要转换数据至List 2.采 ...
- Java之Property-统获取一个应用程序运行的次数
package FileDemo; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStre ...