31、高效开发:文件操作与并发编程实战

高效开发:文件操作与并发编程实战

1. 文件操作与编码设置

在开发过程中,文件操作是常见需求。以弹出按钮设置文件编码为例,可按以下步骤操作:
- 从库中拖动 NSDictionaryController 到 nib 窗口,重命名为 strEncs
- 将 strEncs Content Dictionary 绑定到应用委托的 encodingNames 键路径。
- 把弹出按钮的 Content 绑定到 strEncs ,使用 arrangedObjects 作为 Controller Key value 作为 Model Key Path
- 绑定弹出按钮的 Selected Object 到应用委托,使用 stringEncodingName 作为键路径。

完成上述设置后,保存更改并运行应用,就可以通过弹出列表选择编码,应用会使用所选编码重新显示文本。

2. 并发编程的挑战与意义

在软件开发中,编写能同时处理多项任务的软件是一项重大挑战。过去,计算机通过快速切换任务来营造并发的假象,而如今多核处理器的出现让计算机真正具备了同时执行多项任务的能力。然而,编写能有效利用多核的应用程序仍然极具技术难度。

当应用程序执行耗时操作时,可能会出现“沙滩球”光标,让用户误以为应用即将崩溃。为避免这种情况,应让耗时操作在后台进行,确保用户仍能与应用进行交互。

3. SlowWorker 应用示例

为演示并发编程的实现方式,我们创建了一个名为 SlowWorker 的简单应用,它模拟了从服务器获取数据和进行计算等耗时操作。以下是创建该应用的步骤:
1. 在 Xcode 中创建一个新的 Cocoa 应用,无需 Core Data 或文档支持,启用垃圾回收。
2. 创建 SlowWorkerAppDelegate 类,并将其添加到 MainMenu.xib 中(如果需要)。
3. 在 SlowWorkerAppDelegate.h 中添加以下代码:

#import <Cocoa/Cocoa.h>
@interface SlowWorkerAppDelegate : NSObject {
    IBOutlet NSButton *startButton;
    IBOutlet NSTextView *resultsTextView;
}
- (IBAction)doWork:(id)sender;
@end
  1. SlowWorkerAppDelegate.m 中添加以下代码:
#import "SlowWorkerAppDelegate.h"
@implementation SlowWorkerAppDelegate
- (NSString *)fetchSomethingFromServer {
    sleep(1);
    return @"Hi there";
}
- (NSString *)processData:(NSString *)data {
    sleep(2);
    return [data uppercaseString];
}
- (NSString *)calculateFirstResult:(NSString *)data {
    sleep(3);
    return [NSString stringWithFormat:@"Number of chars: %d",
            [data length]];
}
- (NSString *)calculateSecondResult:(NSString *)data {
    sleep(4);
    return [data stringByReplacingOccurrencesOfString:@"E"
                                            withString:@"e"];
}
- (IBAction)doWork:(id)sender {
    NSDate *startTime = [NSDate date];
    NSString *fetchedData = [self fetchSomethingFromServer];
    NSString *processed = [self processData:fetchedData];
    NSString *firstResult = [self calculateFirstResult:processed];
    NSString *secondResult = [self calculateSecondResult:processed];
    NSString *resultsSummary = [NSString stringWithFormat:
                                @"First: [%@]\nSecond: [%@]", firstResult, secondResult];
    [resultsTextView setString:resultsSummary];
    NSDate *endTime = [NSDate date];
    NSLog(@"Completed in %f seconds",
          [endTime timeIntervalSinceDate:startTime]);
}
@end
  1. 打开 MainMenu.xib ,将 NSButton NSTextView 放入窗口,连接应用委托的出口和按钮的动作。
  2. 配置 NSTextView ,删除示例文本并关闭可编辑选项。

运行应用后,点击按钮会发现应用在执行任务时变得无响应,鼠标光标变为“沙滩球”,这正是我们需要解决的问题。

4. 并发编程基础:线程

在现代操作系统中,除了进程的概念,还有执行线程的概念。每个进程可以由多个线程组成,这些线程可以并发运行。如果只有一个处理器核心,操作系统会在多个线程之间切换执行;如果有多个核心,线程会像进程一样分布在各个核心上。

线程之间共享相同的可执行程序代码和全局数据,但每个线程也可以有自己的专属数据。为了确保多个线程访问相同数据时的正确性,可以使用互斥锁(mutex)或锁来保证特定代码块不会被多个线程同时执行。

需要注意的是,Cocoa 中的 AppKit 框架大多不是线程安全的,因此所有与 AppKit 对象相关的方法调用都应该在主线程中执行。

5. 操作队列的使用

为了解决 SlowWorker 应用的响应问题,我们可以使用 NSOperation NSOperationQueue 来实现并发。具体步骤如下:
1. 复制 SlowWorker 项目文件夹,保留原始版本以备后续使用。
2. 在 SlowWorkerAppDelegate.h 中添加以下实例变量和属性:

#import <Cocoa/Cocoa.h>
@interface SlowWorkerAppDelegate : NSObject {
    IBOutlet NSButton *startButton;
    IBOutlet NSTextView *resultsTextView;
    NSString *fetchedData;
    NSString *processed;
    NSString *firstResult;
    NSString *secondResult;
    BOOL isWorking;
    NSDate *startTime;
}
@property (retain) NSString *fetchedData;
@property (retain) NSString *processed;
@property (retain) NSString *firstResult;
@property (retain) NSString *secondResult;
@property (assign) BOOL isWorking;
- (IBAction)doWork:(id)sender;
@end
  1. SlowWorkerAppDelegate.m 中添加 @synthesize 声明,并替换旧的工作方法:
#import "SlowWorkerAppDelegate.h"
@implementation SlowWorkerAppDelegate
@synthesize fetchedData;
@synthesize processed;
@synthesize firstResult;
@synthesize secondResult;
@synthesize isWorking;
- (void)fetchSomethingFromServer {
    sleep(1);
    self.fetchedData = @"Hi there";
}
- (void)processData {
    sleep(2);
    self.processed = [self.fetchedData uppercaseString];
}
- (void)calculateFirstResult {
    sleep(3);
    self.firstResult = [NSString stringWithFormat:@"Number of chars: %d",
                        [self.processed length]];
}
- (void)calculateSecondResult {
    sleep(4);
    self.secondResult = [self.processed stringByReplacingOccurrencesOfString:@"E"
                                            withString:@"e"];
}
- (void)finishWorking {
    NSString *resultsSummary = [NSString stringWithFormat:
                                @"First: [%@]\nSecond: [%@]",
                                self.firstResult, self.secondResult];
    [resultsTextView setString:resultsSummary];
    NSDate *endTime = [NSDate date];
    NSLog(@"Completed in %f seconds",
          [endTime timeIntervalSinceDate:startTime]);
    self.isWorking = NO;
}
- (IBAction)doWork:(id)sender {
    startTime = [NSDate date];
    self.isWorking = YES;
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSOperation *fetch =
    [[NSInvocationOperation alloc] initWithTarget:self
    selector:@selector(fetchSomethingFromServer) object:nil];
    NSOperation *process =
    [[NSInvocationOperation alloc] initWithTarget:self
    selector:@selector(processData) object:nil];
    NSOperation *calculateFirst =
    [[NSInvocationOperation alloc] initWithTarget:self
    selector:@selector(calculateFirstResult) object:nil];
    NSOperation *calculateSecond =
    [[NSInvocationOperation alloc] initWithTarget:self
    selector:@selector(calculateSecondResult) object:nil];
    NSOperation *show =
    [[NSInvocationOperation alloc] initWithTarget:self
    selector:@selector(finishWorking) object:nil];
    [process addDependency:fetch];
    [calculateFirst addDependency:process];
    [calculateSecond addDependency:process];
    [show addDependency:calculateFirst];
    [show addDependency:calculateSecond];
    [queue addOperation:fetch];
    [queue addOperation:process];
    [queue addOperation:calculateFirst];
    [queue addOperation:calculateSecond];
    [queue addOperation:show];
}
@end
  1. 为了简化操作创建代码,我们可以扩展 NSObject 类,添加一个新的方法:
// FoundationAdditions.h
#import <Cocoa/Cocoa.h>
@interface NSObject (SlowWorkerExtras)
- (NSInvocationOperation*)operationForSelector:(SEL)selector;
@end
// FoundationAdditions.m
#import "FoundationAdditions.h"
@implementation NSObject (SlowWorkerExtras)
- (NSInvocationOperation*)operationForSelector:(SEL)selector {
    return [[[NSInvocationOperation alloc] initWithTarget:self
    selector:selector object:nil] autorelease];
}
@end
  1. SlowWorkerAppDelegate.m 中导入 FoundationAdditions.h ,并替换操作创建代码:
NSOperation *fetch =
[self operationForSelector:@selector(fetchSomethingFromServer)];
NSOperation *process =
[self operationForSelector:@selector(processData)];
NSOperation *calculateFirst =
[self operationForSelector:@selector(calculateFirstResult)];
NSOperation *calculateSecond =
[self operationForSelector:@selector(calculateSecondResult)];
NSOperation *show =
[self operationForSelector:@selector(finishWorking)];
6. 确保在主线程执行

由于 finishWorking 方法需要更新 GUI,而 AppKit 对象的操作应该在主线程中进行,因此我们需要确保该方法在主线程中执行。可以使用以下代码实现:

- (void)finishWorking {
    if(![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(finishWorking)
        withObject:nil waitUntilDone:NO];
        return;
    }
    NSString *resultsSummary = [NSString stringWithFormat:
                                @"First: [%@]\nSecond: [%@]",
                                self.firstResult, self.secondResult];
    [resultsTextView setString:resultsSummary];
    NSDate *endTime = [NSDate date];
    NSLog(@"Completed in %f seconds",
          [endTime timeIntervalSinceDate:startTime]);
    self.isWorking = NO;
}

为了简化代码,我们可以将上述逻辑封装成一个 C 预处理器宏:

#define DISPATCH_ON_MAIN_THREAD if(![NSThread isMainThread]) { \
[self performSelectorOnMainThread:_cmd withObject:nil \
waitUntilDone:NO]; \
return; }

然后在 finishWorking 方法中使用该宏:

- (void)finishWorking {
    DISPATCH_ON_MAIN_THREAD
    NSString *resultsSummary = [NSString stringWithFormat:
                                @"First: [%@]\nSecond: [%@]",
                                self.firstResult, self.secondResult];
    [resultsTextView setString:resultsSummary];
    NSDate *endTime = [NSDate date];
    NSLog(@"Completed in %f seconds",
          [endTime timeIntervalSinceDate:startTime]);
    self.isWorking = NO;
}
7. 进度指示器的配置

为了让用户更直观地了解任务的执行进度,我们可以添加进度指示器。以下是添加圆形和水平进度指示器的步骤:
1. 在 MainMenu.xib 中添加圆形进度指示器,关闭 Display When Stopped 选项。
2. 绑定进度指示器的 Animate 属性到应用委托的 isWorking 键。
3. 绑定 Start 按钮的 Enabled 属性到应用委托的 isWorking 键,并添加 NSNegateBoolean 作为值转换器。
4. 在 SlowWorkerAppDelegate.h .m 文件中添加 completed 实例变量和属性:

// SlowWorkerAppDelegate.h
NSInteger completed;
@property (assign) NSInteger completed;
// SlowWorkerAppDelegate.m
@synthesize completed;
  1. doWork: 方法中重置 completed 变量:
self.completed = 0;
  1. 为了避免多线程环境下的竞态条件,我们使用 @synchronized 关键字来确保 completed 变量的正确更新:
- (void)incrementCompleted {
    @synchronized(self) {
        self.completed = self.completed + 1;
    }
}
  1. 在每个工作方法的末尾调用 [self incrementCompleted];
  2. MainMenu.xib 中添加水平进度指示器,设置其最小值为 0,最大值为 4。
  3. 绑定水平进度指示器的 Animate 属性到应用委托的 isWorking 键, Value 属性到应用委托的 completed 键。

完成上述配置后,运行应用并点击 Start 按钮,你会看到按钮变为禁用状态,圆形进度指示器开始旋转,水平进度指示器的进度条随着工作方法的完成而移动。当任务完成时,进度指示器消失,按钮恢复正常。

通过以上步骤,我们成功地解决了 SlowWorker 应用的响应问题,实现了并发编程,让应用在执行耗时操作时仍能保持响应。

高效开发:文件操作与并发编程实战

8. 操作队列与并发编程的优势总结

使用操作队列和并发编程为应用开发带来了显著的优势,下面通过表格形式进行总结:
|优势|说明|
| ---- | ---- |
|提升响应性|将耗时操作放入后台线程执行,避免主线程阻塞,使应用在执行任务时仍能响应用户操作,如 SlowWorker 应用中使用操作队列后,点击按钮不再出现长时间无响应和“沙滩球”光标。|
|资源利用优化|在多核处理器环境下,操作队列可以将任务分配到多个核心上并行执行,充分利用系统资源,提高任务执行效率。|
|代码可维护性增强|将任务拆分成独立的操作,通过设置操作之间的依赖关系,使代码结构更加清晰,易于理解和维护。例如在 SlowWorker 应用中,每个工作方法对应一个操作,操作之间的依赖关系明确。|
|易于扩展|可以方便地添加、删除或修改操作,根据需求调整任务执行顺序和并发度,适应不同的业务场景。|

9. 并发编程的潜在问题与解决方案

虽然并发编程带来了诸多好处,但也存在一些潜在问题,下面通过流程图展示问题及对应的解决方案:

graph TD
    A[并发编程潜在问题] --> B[数据竞争]
    A --> C[死锁]
    A --> D[线程安全问题]
    B --> E[使用互斥锁或同步机制]
    C --> F[合理设计锁的获取顺序]
    D --> G[确保关键代码在主线程执行]
    D --> H[使用线程安全的数据结构]
  • 数据竞争 :多个线程同时访问和修改共享数据可能导致数据不一致的问题。解决方案是使用互斥锁(如 @synchronized 关键字)或其他同步机制,确保同一时间只有一个线程可以访问和修改共享数据。例如在 SlowWorker 应用中,使用 @synchronized 关键字确保 completed 变量的正确更新。
  • 死锁 :当多个线程相互等待对方释放锁时,可能会导致死锁,使程序陷入无限等待状态。解决方案是合理设计锁的获取顺序,避免循环等待。
  • 线程安全问题 :由于 AppKit 框架大多不是线程安全的,访问 AppKit 对象的操作应该在主线程中执行。可以使用 NSThread 类的方法确保方法在主线程中执行,或者使用线程安全的数据结构。
10. 并发编程的应用场景拓展

并发编程在实际开发中有广泛的应用场景,下面列举一些常见的场景:
- 网络请求 :在应用中进行网络请求时,由于网络请求通常需要一定的时间,为了避免阻塞主线程,影响用户体验,可以将网络请求操作放入后台线程执行。例如在一个新闻应用中,获取新闻数据的网络请求可以在后台线程进行,同时主线程继续响应用户的界面操作。
- 数据处理 :当需要处理大量数据时,如数据解析、数据加密等操作,这些操作可能会消耗大量的时间和系统资源。使用并发编程可以将数据处理任务分配到多个线程中并行执行,提高处理效率。例如在一个数据分析应用中,对大量数据进行统计和分析的操作可以使用并发编程来加速处理过程。
- 多媒体处理 :在多媒体应用中,如视频播放、图片处理等,需要进行大量的计算和数据处理。并发编程可以将这些任务分配到多个线程中,确保多媒体的流畅播放和处理。例如在一个视频编辑应用中,视频的解码、剪辑和编码等操作可以在不同的线程中并行执行。

11. 代码优化建议

在使用操作队列和并发编程时,还可以进行一些代码优化,以进一步提高性能和可维护性:
- 减少锁的使用范围 :尽量缩小锁的使用范围,避免不必要的锁竞争。例如在 incrementCompleted 方法中,只对 self.completed = self.completed + 1; 这一行代码使用 @synchronized 关键字,而不是将整个方法都加锁。
- 使用更高效的同步机制 :除了 @synchronized 关键字,还可以使用更高效的同步机制,如 NSLock NSRecursiveLock 等。这些锁提供了更多的控制选项,可以根据具体需求选择合适的锁。
- 合理设置操作队列的并发度 :根据系统资源和任务的特点,合理设置操作队列的最大并发操作数。例如在一个多核处理器的系统中,可以适当增加操作队列的并发度,以充分利用多核资源。

12. 总结

通过对文件操作、编码设置以及并发编程的学习和实践,我们掌握了如何使用 NSOperation NSOperationQueue 实现并发编程,解决了应用在执行耗时操作时的响应问题。同时,我们也了解了并发编程的基础知识、潜在问题及解决方案,以及并发编程在不同场景下的应用。在实际开发中,我们可以根据具体需求灵活运用这些知识和技术,提高应用的性能和用户体验。希望本文能为你在开发过程中遇到的并发编程问题提供一些思路和解决方案。

通过以上内容,我们深入探讨了文件操作和并发编程的相关知识,从基础概念到实际应用,再到代码优化和问题解决,全面展示了如何在应用开发中实现高效的并发编程。在实际开发中,我们可以根据具体需求灵活运用这些技术,不断优化应用的性能和用户体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值