在计算机科学中,并发处理(concurrent processing)是指同时执行多个逻辑控制流(在软件中实现的)。在计算机系统中,并发处理可以在从硬件层到应用层的多个层级中实现。在应用层使用并发处理,可以开发出以并行方式(in parallel)执行多个操作的应用程序,这些操作包括回应异步事件、访问I/O设备、提供网络服务以及进行并行运算等
1.1 并发编程的基本原则
并发编程是一个含有许多概念和创意的广阔领域。因此,在学习这种技术前,了解一些基本术语和各种并发编程的设计概念及优点就非常重要。下面先理解下并发处理与顺序处理的区别。如图:
并发处理就意味着同时执行多个任务。但实际上,在设计时希望利用并发机制的程序是否真的会并发执行多任务,取决于运行程序的计算机系统。这就引出了一个必须说明的差异,即并发计算(concurrent computing)和并行计算(parallel computing)的差异。从广义上将,并发计算与设计相关,而并行计算与硬件相关。
并行计算 or 并发计算
- 并行计算:指多个软件同时执行多个操作和任务。执行并行计算的能力直接取决于计算机硬件。当今的大多数计算机都拥有多个核心或多个CPU,使得它们能够执行多条指令。
- 并发计算:指一个软件被设计和实现来同时执行多个操作或任务。如果你的软件是并发编程原则和机制设计并实现的,那么它的某些/所有组件就会根据计算机硬件的能力,以并发方式运行。因此,要发挥并发处理的优势,就必须以相应的方式设计和实现软件,并在能够支持并行处理的硬件上运行它
并发编程 or 异步编程
- 并发处理:是指同时处理多个逻辑控制流。
- 异步处理:是一种对方法和函数进行异步(非阻塞)调用操作的高效机制。换言之,调用了方法后,在该方法被执行时,调用程序仍旧可以继续其处理过程。这种方式抽象化了基础实现机制,可以提高应用程序的响应性、系统的吞吐量等性能指标。可以通过多种设备实现异步处理,也可以通过使用并发编程API和服务来实现。
1.1.1 并发处理的优势
以应用程序级并发处理方式开发程序,可以使程序同时执行多个操作。然而,这些功能并不是没有代价的,要实现这些功能,不仅需要有计算机硬件的支持,而且必须将并行处理思想融入到软件设计和实现过程之中。包括但不限于以下几条:
- 增加应用程序的吞吐量:因为并发处理会使程序同时处理多个任务,而应用程序的吞吐量就是指在一段时间内应用程序能够完成的任务数,所以并发处理会比顺序处理完成更多任务。
- 提高系统利用率:以并行方式执行多个任务,可以更集中、更高效地利用系统资源。
- 提供应用程序的整体响应性:以并发方式执行多个任务时,如果某个任务(如输入操作)正在等待,那么其他任务也可以继续运行,婴儿能够减少应用程序的整体闲置时间,提高应用程序的响应性。
- 更好地与问题领域契合:在对某些问题(如科学、数学和人工智能领域的)进行建模时,可以将它们创建为同时处理的任务集合。以并发编程方式处理这些模型是更自然、更优的选择。
1.1.2 实现并发处理
在计算机系统中,实现并发处理的方式有很多。下面是几种常见的方式:
- 分布式计算:在这种并发处理方式中,多个任务会被分给多台通过网络相连的计算机执行,这些计算机通过消息传递来相互通信。
- 并行编程:在这种并发处理方式中,通常由多核CPU和可编程GPU进行大量的并行计算。
- 多进程:在这种并发处理方式中,多个任务会被分发给一台计算机中的多个进程,每个进程都拥有由操作系统管理的独立资源和地址空间。
- 多线程:在这种并发处理方式中,多个任务会与多个线程对应,这些线程会被配置为以并发方式执行。因为这些线程是在单个程序(进程)的环境中被执行的,所以它们会共享资源(如地址空间和内存等)。
1.2 并发处理带来的挑战
正确的使用并发编程并不容易,主要的难点是进行同步操作和在并发执行的控制线程(即逻辑控制流)之间共享信息。在控制不同线程中的相关操作时需要实现同步,而要在线程之间进行通信就必须实现信息共享。
可通过许多机制处理上述难题,其中最常用的两种是共享内存和消息传递。共享内存编程模式会实现共享状态,也就是说,多个线程都可以访问某些程序数据。当程序中的多个线程共同使用某个地址空间时,共享内存就成了信息共享方式的自然自选,既快速又高效。
1.2.1 共享数据
共享内存模式需要一种机制来协调多个线程共用的数据。通常使用同步机制来实现这一目标,例如使用锁或判定条件。锁 是一种控制多线程间数据访问和资源共享的机制。线程获得共享资源的锁,对该资源执行操作,接着释放这个锁,然后其他线程才能访问该资源。条件变量 是一种同步机制,它使线程一直处于等待状态直到指定条件出现。条件变量通常是使用锁实现的。
1.2.2 锁带来的问题
锁是最常见的控制机制,使用它可以控制多线程对共享数据的访问。锁实施一种互斥策略,从而避免受保护的数据和资源被多个线程同时访问。遗憾的是,在使用锁协调对共享数据的访问时,很可能引发死锁、或锁和资源匮乏问题,这些问题都会导致程序中断。
- 死锁:是指两个或多个线程相互阻塞的情况,每个线程都在等待其他线程释放锁,导致所有线程都一直处于等待状态。死锁的一个例子是循环等待。如上图
- 活锁:是指一个线程因为要回应其他的一个或多个线程,而导致自身无法继续执行的情况。活锁的线程没有被阻塞,它将所有的计算时间用于回应其他线程,以恢复正常的操作
- 资源匮乏:是指线程无法访问共享资源的情况,其原因是共享资源被其他线程占用。当一个或多个线程占用共享资源的时间过长时,就会引发这种问题。实际上,可以将活锁视为资源匮乏的一种形式。
下面是一些防止出现这类问题的常用处理方式
- 实现获取锁的总次序:确保进程按照固定次序获取和释放锁。
- 防止出现保持和等待条件:使线程一次原子获取所有锁。这可以确保在任何时候每个线程都拥有一个锁,从而使程序获得了全局预防锁。这种处理方式消除了出现保持和等待情况的可能性,但可能会降低并发处理的效率,而且需要掌握线程代码的知识。
- 提供优先权:使用提供试锁(trylock)或类似机制的锁,如果可以,获取锁。如果不行,返回一个合适的结果。这种处理方式会增加出现活锁的可能性,而且需要掌握代码如何使用锁的知识。
- 设置等待超时:使用提供超时功能的锁,防止出现无限等待的情况。
1.2.3 消息传递
在消息传递模式中,模型状态不是共享的,线程通过交换消息进行通信。这种处理方式使线程能够通过交换消息进行同步和通信。消息传递避免了互斥问题,并自然地与多核、多处理器系统契合。使用消息传递既可以执行同步通信,可以执行异步通信。
在进行同步消息传递时,发送者和接收者会直接连接;消息传递操作完成后,发送者和接收者会断开连接。
异步消息传递通过队列传输消息,而不是在线程之间直接传递。因此,发送者和接收者并不会配对,发送者将消息发送给队列后也无需断开连接。
1.3 在 Objective-C 中实现并发编程
- 语言特性:Objective-C语言有多个支持并发编程的特性。使用@synchronized指令可以在Objective-C代码中创建锁。使用atomic属性限定符可以对Objective-C属性进行线程安全的访问
- 消息传递:Foundation框架中的NSObject类含有多个用于向其他线程发送消息的方法。这些方法会将目标线程运行循环中的消息添加到队列中,而且能够通过通过或异步方式执行。
- 线程:Foundation框架提供了直接创建和管理线程的整套API。其中还包括用于多线程共享数据进行同步访问的Foundation框架API集。
- 操作队列:这是基于Objective-C的消息传递机制,它通过异步设计方法实现并发编程
- 分派队列:这些是基于C语言的一系列语言特性和运行时服务,用于通过异步和并发方式执行任务
1.4 语言特性
@synchronized指令提供了在Objective-C代码中创建锁的简单机制,使并发线程能够同步访问共享状态。
@synchronized指令后面带有一个放在圆括号中的唯一标识符,以及放在花括号中的受保护代码块。这个唯一标识符是一个用于区分受保护代码块的对象。如果有多个线程尝试使用想用的唯一标识符访问这个关键部分,那么这些线程中的某一个线程会先得到锁,而其他线程会被阻塞,直到得到锁的线程完成了对这个关键部分的操作为止。
// 申明userName属性
@property (nonatomic, copy) NSString *userName;
// 可以这样修改属性值
@synchronized (_userName) {
// 关键部分 - 被该指令保护的代码
_userName = @"yuwenhua";
}
Objective-C语言还提供了一种用于对属性进行原子访问的特性。原子(atomic)是指不论属性是否被以并发方式访问,属性的访问方法永远都会设置/获取完整(一致性)值。
@property (atomic, copy) NSString *userName;
1.5 消息传递
Foundation框架中的NSObject类含有许多方法,这些方法使用消息传递模式,通过线程调用对象中的方法。可以是主线程或其他线程。
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
......
NSObject类中的每个方法都会为接受对象中将被线程调用的方法设置选择器。该方法也叫做线程入口例程。选择器消息会在线程的运行循环中排队,而该方法会作为运行循环中的标准处理过程被线程执行。使用这些消息传递方法,可以设置为同步或异步方式调用线程。同步调用方法会阻塞当前线程,直到该方法执行完为止。因为这些方法是NSObject类定义的,所有NSObject类的所有子类都会拥有这些方法。
代码清单 使用NSObject类中的performSelector:onThread: withObject:waitUntilDone: 异步调用线程secondaryThread中的downloadTask方法
Processor *processor = [[Processor alloc] init];
[processor performSelector:@selector(downloadTask) onThread:secondaryThread withObject:nil waitUntilDone:NO];
在创建线程时,可以配置它的部分运行时环境(如栈大小、本地线程存储空间、线程优先权等)。通过使用下列功能实现线程入口点例程:自动释放池、异常处理程序、运行循环。
NSObject类中的performSelectorOnMainThread:方法通常用于,从次要线程对象向主线程对象返回值(如状态、计算结果等)。这样就在次要线程和主线程之间实现了通信。
1.6 线程
线程是指在某个进程环境中执行的逻辑控制流。OS X和iOS操作系统为线程的创建、管理和执行提供了直接的支持。在应用层,Foundation框架提供了许多用于创建和管理线程的API,以及用于在并发线程之间同步共享数据访问的API集合
1.6.1 NSObject线程
使用NSObject类中的performSelectorInBackground:withObject:方法,可以隐式地创建和启动用于执行对象中方法的新线城。该线程会作为后台次要线程立刻启动,而当前进程会立刻返回。这个方法提供了一种使用新后台线程执行对象中方法的简单机制。该线程实例是隐式创建的,因此无需直接使用API。应根据需要,通过自动释放此、异步处理器和运行时循环在方法中配置该线程的环境
Processor *processor = [[Processor alloc] init];
[processor performSelectorInBackground:@selector(downloadTask) withObject:nil];
1.6.2 NSThread
NSThread类提供了用于通过显式创建和管理线程的API。该类含有多个方法,使用这些方法可以创建和初始化NSThread对象、启动和停止线程、配置线程和查询线程及其执行环境
下面是NSThread类中用于创建和初始化线程的API:
detachNewThreadSelector:toTarget:withObject:
intiWithTarget:selector:object:
该方法会创建新线程并调用接收者的入口点例程(即将该方法与它的选择器对应起来)。不同的是 intiWithTarget:selector:object: 方法会创建新线程,但不会启动该线程。当已初始化的线程开始执行接收者的入口点例程时,调用NSThread类的 start 方法启动该线程
NSThread *newThread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadTask) object:nil];
[newThread setThreadPriority:0.5];
[newThread start];
// 线程暂停5秒
[NSThread sleepForTimeInterval:5.0];
1.6.3 线程同步
Objective-C平台提供了多种机制来管理共享状态是实现线程之间的同步。Foundation框架中含有一系列锁和条件变量API。
1. 锁
Foundation框架中含有多个类(NSLock、NSRecursiveLock、NSConditionLock、NSDistributedLock),使用这些类可以实现各种用于同步访问共享状态操作的锁。锁用于保护关键部分(即用于访问共享数据或资源的代码部分),这些代码不允许多个线程以并发方式执行。
NSLock类为并发编程实现了一种基本的互斥锁。它遵守NSLocking协议,并因此会实现分别用于获取和释放锁的 lock 和 unlock 方法。前面介绍过@synchronized,它是一种Objective-C语言特性,可以实现媲美NSLock类的互斥锁。主要区别:
- @synchronized指令隐式创建锁,而NSLock类的API直接创建锁
- @synchronized指令会隐式地为关键部分提供异常处理程序,而NSLock类没有提供这一功能
代码清单 使用NSLock实例保护关键部分
NSLock *computeLock = [NSLock new];
...
[computeLock lock];
// 关键部分
...
[computeLock unlock];
NSDistributedLock类定义了一个可由多台主机上的多个应用程序使用的锁,使用该锁可以控制对共享资源的访问操作。与NSLock类不同,NSDistributedLock实例没有实施互斥策略,而是在锁处于繁忙状态时发送报告,由使用锁的代码根据锁的状态适当地执行操作。下面的代码用一个文件的路径 /hello.lck 创建了分布锁,你可能会将该文件作为锁定系统对象。
代码清单 使用NSDistributedLock实例控制对资源的访问
NSDistributedLock *fileLock = [NSDistributedLock lockWithPath:@"/hello.lck"];
// 访问资源
...
// 解除对资源的锁定
[fileLock unlock];
NSDistributedLock类没有遵守NSLocking协议。而且,因为这个锁是使用文件系统实现的,所以它必须显示释放。如果某个应用程序在拥有分布锁的情况下终止运行,那么其他客户端必须使用NSDistributedLock类中的 breakLock 方法才能解除这种锁定
NSConditionLock类定义了一种只有在特定条件下才能获取和释放的锁,这个条件是由你定义的整数值。条件锁通常用于确保任务以指定的顺序执行,如线程之间的厂商-消费者处理流。
代码清单 使用NSConditionLock实例控制对资源的访问
NSConditionLock *dataLock = [[NSConditionLock alloc] initWithCondition:NO];
...
// 获取锁(缓冲区中没有数据)
[dataLock lock];
// 将数据添加到缓冲区中
...
// 根据条件解锁(数据位于缓冲区中)
[dataLock unlockWithCondition:YES];
NSRecursiveLock类定义了一种在不引起死锁的情况下,可以被同一个线程获取多次的锁。
2. 条件
条件变量是一种锁,用于同步操作的执行顺序。尝试获取条件的线程会一直阻塞,直到另一个线程显式地向该条件发出信号(signla)为之。等待某个条件的线程也会一直阻塞,直到另一个线程显式的向该条件发出信号为之。Foundation框架中NSCondition类实现了一种条件变量。
使用NSCondition实例同步对共享数据的消费者操作
@autoreleasepool {
// 获取条件锁并测试布尔条件
[condition lock];
while (!self.dataAvailable) {
[condition wait];
}
// 数据处于可访问状态后,对数据进行处理(代码已省略)
...
// 完成处理数据的操作,更新判断值和发送信号的条件
self.dataAvailable = NO;
[condition signal];
// 解除条件锁
[condition unlock];
}
1.6.4 使用线程实现并发处理
下面使用线程和这些同步机制创建一个执行并发处理的示例程序。
代码清单 ConcurrentProcessor.h
@interface ConcurrentProcessor : NSObject
@property (readwrite) BOOL isFinished;
@property (readonly) NSInteger computeResult;
- (void)computeTask:(id)data;
@end
代码清单 ConcurrentProcessor.m
@interface ConcurrentProcessor ()
@property (readwrite) NSInteger computeResult;
@end
@implementation ConcurrentProcessor
{
NSString *computeID; // @synchronize 指令锁定的唯一对象
NSUInteger computeTask; // 并行计算任务的计数
NSLock *computeLock; // 锁对象
}
- (id)init {
if (self = [super init]) {
_isFinished = NO;
_computeResult = 0;
computeLock = [NSLock new];
computeID = @"1";
computeTask = 0;
}
return self;
}
- (void)computeTask:(id)data {
NSAssert(([data isKindOfClass:[NSNumber class]]), @"Not an NSNumber instance");
NSInteger computations = [data integerValue];
@autoreleasepool {
@try {
// 获取所并增加活动任务的计数
if ([[NSThread currentThread] isCancelled]) {
return;
}
@synchronized (computeID) {
computeTask++;
}
// 获取所并执行关键代码部分中的计算操作
[computeLock lock];
if ([[NSThread currentThread] isCancelled]) {
[computeLock unlock];
return;
}
NSLog(@"Performing computations");
for (NSInteger ii=0; ii
代码清单,客户端代码,在viewDidLoad中添加下面代码
ConcurrentProcessor *processor = [[ConcurrentProcessor alloc] init];
[processor performSelectorInBackground:@selector(computeTask:) withObject:[NSNumber numberWithInteger:5]];
[processor performSelectorInBackground:@selector(computeTask:) withObject:[NSNumber numberWithInteger:10]];
[processor performSelectorInBackground:@selector(computeTask:) withObject:[NSNumber numberWithInteger:20]];
while (!processor.isFinished);
NSLog(@"Computation result5 = %d",processor.computeResult);
客户端输出
1.7 操作和操作队列
1.7.1 操作
NSOperation、NSBlockOperation和NSInvocationOperation类用于管理一个或多个操作、代码以及单个任务关联数据的并行执行过程。操作队列是指提供并行执行任务功能的Objective-C对象。每个任务(即操作)都定义了需要执行的程序和与之相关的数据,而且会被封装在块对象或NSOperation类的具体子类中。NSOperation是一个抽象类,用于封装单个任务和代码和相关数据。在处理并行任务时,具体子类通常只需重写主要方法。至少必须重写 start 、inConcurrent 、 isExecuting 和 isFinished 方法。
下面的语句创建了一个名为greetingOp的NSBlockOperation实例
NSBlockOperation *greetingOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Hello, World!");
}];
还可以使用addExecutionBlock:方法,为NSBlockOperation实例添加额外的块兑现
[greetingOp addExecutionBlock:^{
NSLog(@"Goodbye");
}];
可以使用NSInvocationOperation类初始化实例。然后调用选择器 hello: ,并带有NSNumber参数值:
NSInvocationOperation *invokeOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(hello:) object:@(20)];
还可以实现自定义操作类。自定义操作类是NSOperation类的子类,至少必须实现用于执行任务的main方法。自定义操作还可以提供下列功能:
- 自定义的初始化方法
- 自定义的辅助方法(通过main方法调用)
- 用于设置数据值和访问操作结果的访问器方法
- 使自定义操作类遵守NSCoding协议的方法(以支持归档对象的功能)
操作对象可以支持各种并发编程功能:
- 在操作对象之间建立依赖关系,从而控制它们的执行顺序
- 创建在操作的主任务完成后执行的完成语句块
- 获取操作的执行状态
- 为操作队列中的操作设置优先权
- 取消操作
操作对象是通过调用其 start 方法执行的。该方法的默认实现会同步执行操作的任务。要实现并发编程的话,可以把操作对象添加到操作队列中执行。
1.7.2 操作队列
操作队列是一种提供并发执行操作能力的机制。Foundation框架中的NSOperationQueue类就是操作队列的一个Objective-C实现。可以将操作作为快对象或NSOperation类的某个子类的实例,添加到NSOperationQueue实例中。操作队列管理操作的执行情况。因此,NSOperationQueue类含有多个方法,使用这些方法可以管理操作队列中的操作、管理正在运行的操作的数量、暂停操作和检索特定的操作队列。
向操作队列中添加一个块对象和一个操作
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 一个块对象
[queue addOperationWithBlock:^{
for (NSInteger nn=0; nn<1000; nn++) {
NSLog(@"NN = %d ---- %@",nn,[NSThread currentThread]);
}
}];
// 一个操作
NSBlockOperation *greetingOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"Hello, World!");
}];
[queue addOperation:greetingOp];
将操作添加到队列中后,它会呆在队列中,直到被显式取消或执行完成它的任务为之。通过调用NSOperation对象的 cancel 方法或者操作队列中的 cancelAllOperations 方法,可以取消操作队列中的NSOperation对象
操作队列中操作的执行顺序取决于操作的优先级和操作对象之间的依赖关系。操作队列中的每个操作都是由独立线程执行的。
1.7.3 实现并发操作 --- 自定义NSOperation子类来实现
代码清单,GreetingOperation类继承自NSOperation类,GreetingOperation.m
@implementation GreetingOperation
{
BOOL finished;
BOOL executing;
}
- (id)init {
if (self = [super init]) {
executing = NO;
finished = NO;
}
return self;
}
- (void)start {
// 如果操作被取消了就返回结果
if ([self isCancelled]) {
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return;
}
// 使用独立线程执行main方法中的操作
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)main {
@autoreleasepool {
@try {
if (![self isCancelled]) {
NSLog(@"Current Thread = %@",[NSThread currentThread]);
NSLog(@"Hello, World!");
[NSThread sleepForTimeInterval:3.0];
NSLog(@"Goodbye, World!");
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];
}
} @catch (NSException *exception) {
} @finally {
}
}
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
@end
客户端代码
ConcurrentOperation *op = [[ConcurrentOperation alloc] init];
[op start];
1.7.4 实现并发操作 --- 使用操作队列实现并发处理
同上,修改客户端代码
ConcurrentOperation *op = [[ConcurrentOperation alloc] init];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:op];
[queue waitUntilAllOperationsAreFinished];
1.8 分派队列
Grand Central Dispatch(GCD)是一个集合,它含有语言特性、基于C语言的API、以及支持使用分派队列执行任务的系统增强功能。使用GCD分派队列可以同步或异步地执行代码,以及串行或并行的执行任务。与操作队列一样,分派队列也比线程更易于使用,执行异步或并发任务更高效。
客户端代码,在viewDidLoad中添加
// 创建顺序队列和分组
dispatch_queue_t serialQueue = dispatch_queue_create("com.ds.turkey", DISPATCH_QUEUE_SERIAL);
dispatch_group_t group = dispatch_group_create();
// 向队列中添加任务
dispatch_group_async(group, serialQueue, ^{
NSLog(@"First task");
[NSThread sleepForTimeInterval:2];
NSLog(@"First task over");
});
dispatch_group_async(group, serialQueue, ^{
NSLog(@"Second task");
[NSThread sleepForTimeInterval:2];
NSLog(@"Second task over");
});
// 等待,当分组中的所有任务都完成时
// 作其他处理
dispatch_group_notify(group, serialQueue, ^{
NSLog(@"所有任务都已经完成");
});
NSLog(@"Over");
// 结果为:
2017-05-27 09:25:35.372 Test_concurrent[3126:74272] Over
2017-05-27 09:25:35.372 Test_concurrent[3126:74358] First task
2017-05-27 09:25:37.375 Test_concurrent[3126:74358] First task over
2017-05-27 09:25:37.376 Test_concurrent[3126:74358] Second task
2017-05-27 09:25:39.377 Test_concurrent[3126:74358] Second task over
2017-05-27 09:25:39.378 Test_concurrent[3126:74358] 所有任务都已经完成
这段代码使用GCD API创建并以异步方式分派了3个串行执行的任务,从而协调了它们的执行次序,防止了它们对共享数据进行并发访问。最后 dispatch_group_async 函数监听任务是否完成,可以作出对结果的处理(更新界面)。