1、什么是并发编程?
并发编程是指在一台处理器上“同时”处理多个任务。它的目标是充分的利用处理器的每一个核,以达到更高的处理性能。
2、并发处理 与 顺序处理
- 顺序处理:按照先后顺序处理逻辑控制流(即逐个处理)
- 并发处理:同时处理多个逻辑控制流
并发处理相对于顺序处理的优势:
- 增加应用程序的吞吐量:应用程序的吞吐量是指在一段时间内应用程序能够完成的任务数;并发程序同时处理多个任务,会比顺序处理完成更多任务。
- 提高系统的利用率:以并发方式执行多个任务,可以更集中、更高效的利用系统资源。
- 提高应用程序的整体响应性:如果某个任务正在等待,使用并发模式,可以让其它任务继续进行,减少程序的整体闲置时间,提高程序的响应性。
- 更好的与问题领域契合:在处理某些问题时,可以将这些任务创建为同时处理的任务集合,以并发方式处理。
并发处理意味着同时执行多个任务。但实际上,利用并发机制的程序是否真的并发执行多个任务,取决于运行程序的计算机系统。这里引出了两条概念:并发计算(concurrent computing)与 并行计算(Parallel Computing)。
3、并发计算与 并行计算
- 并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。
- 并发:当有多个线程在操作时,如果系统只有一个CPU,则它不能同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态。
从广义上来说:并发计算是软件层面的,与设计相关;并行计算是硬件层面的,与多核CPU相关。
- 并行计算 :指计算机同时执行多个操作或任务。执行并行计算的能力直接取决于计算机硬件,如多核 CPU 同时执行多条指令
- 并发计算:指一个程序被设计同时执行多个操作或任务。如果程序使用并发编程设计并实现的,那么它就会根据计算机硬件的能力,以并发的方式运行。
要发挥并发处理的优势,必须以并发编程设计并实现程序,并在能够支持并行处理的硬件运行它。
4、实现并发处理
在了解了并发处理的优势后,我们要学会如何利用它。在计算机系统中,实现并发处理的方式有很多:
- 分布式计算:多个任务被分给多台网络相连的计算机执行,这些计算机通过消息传递来实现通信;
- 并行编程:由多核CPU和可编程GPU进行大量的并行计算;
- 多进程:多个任务被分给一台计算机的多个进程,每个进程都拥有由操作系统管理的独立资源和地址空间;
- 多线程:多个任务与多个线程对应,这些线程被配置为并发方式执行。这些线程是在单个进程的环境中执行的,它们共享地址空间和内存。
5、 CPU 与 GPU
上文提到 CPU 与 GPU ,那么什么是 CPU ?什么是 GPU ?CPU 与 GPU 的区别是什么?
- CPU 是中央处理器,所做的工作都在软件层面;
- GPU 是图形处理器,所做的工作都在硬件层面;
CPU 擅长处理具有复杂计算步骤和复杂数据依赖的计算任务,如分布式计算,数据压缩,人工智能,物理模拟,以及其他很多很多计算任务等。
区别: CUP 和 GPU 之所以大不相同,是由于其设计目的的不同,它们分别针对了两种不同的应用场景:
- CPU 需要很强的通用性来处理各种不同的类型数据,同时又要逻辑判断又会引入大量的分支跳转和中断处理。这些都使得CPU的内部结构异常复杂。
- GPU面对的则是类型高度统一的、相互无依赖的大规模数据和不需要被打断纯净的计算环境。
6、进程 与 线程
进程与进程都是操作系统的基本概念:
- 进程:正在运行的、拥有独立地址空间和系统资源的计算机程序;是系统进行资源分配和调度的基本单位,是操作系统结构的基础,主要管理资源。
- 线程:在某个进程环境中执行的逻辑控制流,它是可以独立执行的指令序列。线程也被称为轻量级进程,它是进程的基本执行单元,多个线程共享一个地址空间;
- 多线程:在同一时刻,一个 CPU 只能处理 1 条线程,但 CPU 可以在多条线程之间快速的切换,只要切换的足够快,就造成了多线程一同执行的假象。多线程通过提高资源使用率来提高系统总体的效率。但是也不要开启太多线程,当线程过多时,会消耗大量 CPU 资源,且每开一条线程本身也是有开销的(在iOS中,主线程站 1 MB 的内存空间,子线程占用 512 KB,可以使用
stackSize
设置线程占用空间,最小是 16 KB,线程创建时间是 90 ms),可能导致卡顿问题。 - 协程:一串比函数粒度还要小的可手动控制的过程;并不是 线程,不会参与 CPU 时间调度,并没有均衡分配到时间。
进程可能包含多个线程,这些线程可以按顺序执行、以并发方式执行,或者混合使用这两种方式执行。
在 Objective-C 语言中,使用 NSTask
与NSThread
管理进程与线程。
6.1、NSTask
创建并管理进程
使用NSTask
可以在 Objective-C 运行时系统中创建并管理进程,NSTask
作为独立进程进行操作,不与其它进程共享内存,包括创建它的进程。一个NSTask
对象只能运行一次,而且其环境需要在它运行以前配置好。
NSTask *task = [[NSTask alloc] init];//使用 init 初始化一个 task
[task setLaunchPath:@"/Users/longlong/Library/Developer/CoreSimulator/Devices/DD8DCEB3-16B7-412A-9F9E-0315A427A638/data/Containers/Bundle/Application/2CDEA0BD-BCB1-40A9-957A-EDE732AE1072/Demo.app/Demo"];//设置启动的程序目录
[task setArguments:@[@"/Users/longlong/Library/Developer/CoreSimulator/Devices/DD8DCEB3-16B7-412A-9F9E-0315A427A638/data/Containers/Bundle/Application/2CDEA0BD-BCB1-40A9-957A-EDE732AE1072/Demo.app/Demo"]];//设置启动需要的参数
[task launch];//启动该进程
NSLog(@"isRunning == %d",[task isRunning]);//使用 isRunning 查询 task 的状态
6.2、NSThread
创建并管理线程
线程是一种操作系统机制,用以并行方式执行多个指令序列。同一个进程中的线程可以共享计算机的内存和其他资源。
使用NSThread
可以创建和控制线程:
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(startChildThread) object:nil];
thread.stackSize = 16;//设置线程占用空间,最小是 16 KB,
thread.name = @"demo.thread2";//设置线程名字
thread.threadPriority = 1.0;//设置线程优先级
[thread start];//开启线程
7、并发处理带来的困难:
虽然并发处理有如此多优点,但要正确实现它并不容易,它主要有以下几个难点:
- 共享信息:在控制不同线程中的相关操作时需要实现同步,而在线程之间进行通信就必须实现信息共享;
- 因为要同时执行多个线程,所以整个程序的执行顺序是不确定的,使用不同的执行顺序执行同一程序得到不同的结果,这会导致并发程序中的bug 难以检测和修复。
我们可以使用共享内存与消息传递 来处理上述问题。共享内存编程模式会实现共享状态,多个线程都可以访问某些数据。
8、共享内存 与 消息传递
8.1、共享内存
共享内存模式需要一种机制来协调多个线程共用的数据,通常使用同步机制来实现这一目标,如 锁或者判定条件。
- 锁:是一种控制多线程间数据同步访问和资源共享的一种机制。
- 条件:它使线程一直处于等待状态直到指定条件出现,条件变量通常用锁来实现。
锁实施是一种互斥策略,避免受保护的数据和资源被多个线程同时访问。使用锁协调对共享数据的访问时,很有可能引发死锁、活锁、资源匮乏等问题,这些问题导致程序中断:
- 死锁:指两个或多个线程互相阻塞的情况,每个线程都等待其他线程释放锁,导致所有线程都处于等待状态。典型的例子就是循环等待;
- 活锁:指一个线程因为要回应其它线程,而导致自身无法执行的情况。活锁的线程没有被阻塞,它将所有的计算时间用于回应其它线程,以恢复正常的操作。
- 资源匮乏:指线程无法正常访问共享资源的情况,通常是共享资源被其它线程占用。当一个或多个线程占用共享资源的时间过长,就会引发这种问题。活锁也是资源匮乏的一种形式。
8.2、消息传递
线程能够通过交换消息进行同步和通信,消息传递避免了互斥问题,并与多核、多处理器系统契合。使用消息传递既可以执行同步通信,也可以执行异步通信。在进行同步消息传递时,发送者和接收者会直接连接;消息传递操作完成后,发送者和接收者会断开连接。异步消息传递通过队列传输消息,如图所示:
使用队列传递消息消息不是在队列之间直接传递,而是通过 消息队列 进行交换。因此,发送者和接收者并不会配对,发送者将消息发给队列后也无需断开连接。使用异步消息传递可以实现并发编程。
框架Foundation
中的NSObject
含有许多方法,这些方法使用消息传递模式,通过线程调用对象的方法。该线程可以是主线程,也可以是分线程
@interface NSObject (NSThreadPerformAdditions)
//waitUntilDone: 参数设置了同步/异步操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
上述方法,参数 waitUntilDone:
设置了同步YES
/异步NO
操作。
9、运行循环RunLoop
RunLoop
是一种基于线程的机制,用于调度任务和协调收到的输入事件。如果程序的线程需要回应入栈事件,就必须将之附到运行循环中,以在新事件出现时唤醒该线程。
10、在 Objective-C 中实现并发编程
在Objective-C中实现并发编程有以下几种方式:
- 线程 NSThread
- 操作队列 NSOperationQueue:基于Objective-C的消息传递机制,通过亦不涉及方法实现并发编程;
- 分派队列 GCD:基于 C 语言的一系列语言特性和运行时服务,用于通过异步和并发执行任务;
参考文章:
并发编程:API 及挑战
iOS 渲染原理解析
iOS离屏渲染的深入研究
协程
网友评论