深入理解块与并发编程
1. 块的基础使用
1.1 块的创建与排序示例
块在编程中是一种强大的工具,能够简洁地实现特定功能。以下是一个使用块对数组进行排序的示例代码:
NSArray *array = [NSArray arrayWithObjects:
@"Amir", @"Mishal", @"Irrum", @"Adam", nil];
NSLog(@"Unsorted Array %@", array);
NSArray *sortedArray = [array sortedArrayUsingComparator:^(NSString *object1, NSString *object2) {
return [object1 compare:object2];
}];
NSLog(@"Sorted Array %@", sortedArray);
在这个示例中,我们创建了一个包含多个字符串的数组,然后使用
sortedArrayUsingComparator
方法结合块对数组进行排序。块接收两个字符串参数
object1
和
object2
,并使用
compare
方法比较它们的大小,最终返回排序后的数组。
1.2 使用 typedef 简化块的定义
当块的定义比较复杂时,直接编写可能会增加出错的概率。这时可以使用
typedef
来简化块的定义。例如:
typedef double (^MKSampleMultiply2BlockRef)(double c, double d);
MKSampleMultiply2BlockRef multiply2 = ^(double c, double d) {
return c * d;
};
printf("%f, %f", multiply2(4, 5), multiply2(5, 2));
这里我们使用
typedef
定义了一个名为
MKSampleMultiply2BlockRef
的块类型,它接收两个
double
类型的参数并返回一个
double
类型的值。之后我们创建了一个该类型的块
multiply2
,并调用它进行乘法运算。
2. 块与变量
2.1 块可访问的变量类型
块在声明时会捕获其创建时的状态,并且可以访问多种类型的变量,具体如下:
- 全局变量,包括封闭作用域内的局部静态变量。
- 全局函数。
- 封闭作用域的参数。
- 函数级别的
__block
变量,这些是可变变量。
- 封闭作用域的非静态变量被捕获为常量。
- Objective-C 实例变量。
- 块内的局部变量。
2.2 不同类型变量在块中的表现
2.2.1 局部变量
局部变量在块中会被复制并保存其在块定义时的状态。以下是一个示例:
typedef double (^MKSampleMultiplyBlockRef)(void);
double a = 10, b = 20;
MKSampleMultiplyBlockRef multiply = ^(void) {
return a * b;
};
NSLog(@"%f", multiply());
a = 20;
b = 50;
NSLog(@"%f", multiply());
在这个示例中,虽然在块定义后修改了
a
和
b
的值,但块内部仍然使用的是块定义时
a
和
b
的值,所以第二次
NSLog
输出的结果是 200,而不是 1000。
2.2.2 全局变量
如果将变量标记为静态(全局),块会访问其最新的值。示例代码如下:
static double a = 10, b = 20;
MKSampleMultiplyBlockRef multiply = ^(void) {
return a * b;
};
NSLog(@"%f", multiply());
a = 20;
b = 50;
NSLog(@"%f", multiply());
这里的
a
和
b
被声明为静态变量,所以在修改它们的值后,块会访问到最新的值,第二次
NSLog
输出的结果是 1000。
2.2.3 参数变量
块中的参数变量和函数的参数变量类似。例如:
typedef double (^MKSampleMultiply2BlockRef)(double c, double d);
MKSampleMultiply2BlockRef multiply2 = ^(double c, double d) {
return c * d;
};
NSLog(@"%f, %f", multiply2(4, 5), multiply2(5, 2));
在这个示例中,块接收两个参数
c
和
d
,并返回它们的乘积。
2.2.4
__block
变量
局部变量在块中是常量,如果需要修改其值,需要将其声明为
__block
变量。以下是一个示例:
__block double c = 3;
MKSampleMultiplyBlockRef multiply = ^(double a, double b) {
c = a * b;
};
如果不将
c
声明为
__block
变量,编译器会报错。需要注意的是,有些变量不能声明为
__block
变量,例如可变长度数组和包含可变长度数组的结构体。
2.2.5 块局部变量
块局部变量和函数内的局部变量类似。示例代码如下:
void (^MKSampleBlockRef)(void) = ^(void) {
double a = 4;
double c = 2;
NSLog(@"%f", a * c);
};
MKSampleBlockRef();
在这个块中,我们定义了两个局部变量
a
和
c
,并计算它们的乘积。
2.3 Objective-C 对象在块中的使用
在 Objective-C 中,块是一等公民,但使用块时需要特别注意内存管理。以下是一些内存管理的规则:
- 如果引用了一个 Objective-C 对象,该对象会被保留。
- 如果通过引用访问实例变量,
self
(执行该方法的对象)会被保留。
- 如果通过值访问实例变量,该变量会被保留。
以下是两个示例:
// 示例 1
NSString *string1 = ^{
return [_theString stringByAppendingString:_theString];
};
// 示例 2
NSString *localObject = _theString;
NSString *string2 = ^{
return [localObject stringByAppendingString:localObject];
};
在示例 1 中,直接在块中访问实例变量
_theString
,会导致
self
被保留;而在示例 2 中,我们先创建了一个局部引用
localObject
指向实例变量,然后在块中使用该局部引用,此时被保留的是
localObject
而不是
self
。
在 C 级别,需要使用
Block_copy()
和
Block_release()
函数来管理块的内存。例如:
MKSampleVoidBlockRef block1 = ^{
NSLog(@"Block1");
};
block1();
MKSampleVoidBlockRef block2 = ^{
NSLog(@"Block2");
};
block2();
Block_release(block2);
block2 = Block_copy(block1);
block2();
3. 并发编程基础
3.1 并发编程的概念
在传统的编程中,代码通常是按顺序依次执行的。但随着计算机硬件的发展,现代设备(如 Mac 和 iOS 设备)通常具有多个处理器核心,这使得同时执行多个任务成为可能。并发编程就是利用这些多核处理器的能力,让程序中的多个任务可以同时执行。
3.2 POSIX 线程
POSIX 线程是一种基本的并发编程技术,它提供了 C 和 Objective-C 两种 API。使用 POSIX 线程编写并发应用需要创建多个线程,但线程编程具有一定的挑战性,因为线程是底层 API,需要手动管理线程,并且线程数量的需求会根据硬件和其他软件的运行情况而变化。
3.3 Grand Central Dispatch (GCD)
为了减轻多核编程的负担,Apple 引入了 Grand Central Dispatch(GCD)。GCD 是一种系统级技术,它可以自动管理线程,让开发者只需专注于编写任务代码。使用 GCD 时,只需将块或函数提交给系统,GCD 会决定需要多少线程以及何时调度它们运行,从而使计算机或设备更高效地运行。
3.4 同步机制
在并发编程中,同步是一个重要的概念。可以将程序中的多条执行路径类比为多车道高速公路上的车辆行驶。为了保证程序的正确性,需要使用同步设备(如标志或互斥锁)来控制对共享资源的访问。
在 Objective-C 中,提供了
@synchronized
关键字来实现同步。示例代码如下:
@synchronized(theObject) {
// 临界区代码
}
这段代码确保了临界区代码在不同线程中串行访问。当定义属性时,如果不指定
nonatomic
关键字,编译器会生成强制互斥的 getter 和 setter 方法。如果确定属性的值不会被多个线程访问,可以添加
nonatomic
属性来提高性能,因为使用
@synchronized
会带来一定的开销。
3.5 使用 NSObject 方法在后台执行代码
如果只是想在后台执行一些代码,
NSObject
提供了一些方法,如
performSelectorInBackground:withObject:
。但使用这些方法有一些限制:
- 必须为 Cocoa 对象创建自动释放池,因为主自动释放池与主线程关联。
- 方法不能返回任何值,并且要么不接受参数,要么只接受一个对象作为参数。方法的签名必须是
- (void)myMethod;
或
- (void)myMethod:(id)myObject;
。
以下是方法的实现和调用示例:
- (void)myBackgroundMethod {
@autoreleasepool {
NSLog(@"My Background Method");
}
}
- (void)myOtherBackgroundMethod:(id)myObject {
@autoreleasepool {
NSLog(@"My Background Method %@", myObject);
}
}
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:nil];
[self performSelectorInBackground:@selector(myOtherBackgroundMethod:) withObject:arugmentObject];
4. 调度队列
4.1 调度队列的类型
GCD 使用调度队列来管理任务的执行,调度队列类似于线程,但使用起来更加简单。调度队列分为三种类型:
| 队列类型 | 特点 |
| ---- | ---- |
| 串行队列 | 每个串行队列按任务分配的顺序依次执行任务,同一时间每个队列只执行一个任务。可以创建任意数量的串行队列,它们可以并行运行。 |
| 并发队列 | 每个并发队列可以同时执行一个或多个任务,任务按分配到队列的顺序开始执行。不能手动创建并发队列,需要使用系统提供的三个队列(高、默认、低优先级)。 |
| 主队列 | 这是应用程序的主队列,任务在程序的主线程上执行。 |
4.2 串行队列的使用
当需要按特定顺序执行一系列任务时,可以使用串行队列。串行队列的任务执行顺序是先进先出(FIFO),并且不会发生死锁。以下是创建和使用串行队列的示例:
dispatch_queue_t my_serial_queue;
my_serial_queue = dispatch_queue_create("com.apress.MySerialQueue1", NULL);
在这个示例中,我们使用
dispatch_queue_create
方法创建了一个名为
com.apress.MySerialQueue1
的串行队列。
4.3 并发队列的使用
并发队列适用于可以并行运行的任务。并发队列的任务也是按 FIFO 顺序开始执行,但任务的完成顺序是不确定的,并且同一时间可以运行的任务数量会根据系统的运行情况而变化。可以使用
dispatch_get_global_queue
方法获取系统提供的并发队列,例如:
dispatch_queue_t myQueue;
myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
4.4 主队列的使用
使用
dispatch_get_main_queue
方法可以访问与应用程序主线程关联的串行队列。但需要注意,在主队列上调度任务时要格外小心,因为这可能会阻塞主应用程序。主队列通常用于同步操作,例如提交多个任务并在它们全部完成后执行某些操作。
4.5 队列的内存管理
调度队列是引用计数对象,可以使用
dispatch_retain()
和
dispatch_release()
方法来修改队列的引用计数。需要注意的是,这些方法只能用于自己创建的队列,对于全局调度队列,发送这些消息会被忽略。如果编写的是使用垃圾回收的 OS X 应用程序,必须手动管理这些队列。
4.6 队列的上下文
可以为调度对象(包括调度队列)分配全局数据上下文。可以将各种类型的数据(如 Objective-C 对象或指针)分配给上下文,但需要手动管理上下文数据的内存。以下是一个示例:
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity:5];
[myContext setObject:@"My Context" forKey:@"title"];
[myContext setObject:[NSNumber numberWithInt:0] forKey:@"value"];
dispatch_set_context(_serial_queue, (__bridge_retained void *)myContext);
在这个示例中,我们创建了一个字典作为上下文,并使用
dispatch_set_context
方法将其分配给串行队列。
4.7 清理函数
当上下文对象需要释放时,可以指定一个清理函数。清理函数的签名必须是
void function_name(void *context);
。示例代码如下:
void myFinalizerFunction(void *context) {
NSLog(@"myFinalizerFunction");
NSMutableDictionary *theData = (__bridge_transfer NSMutableDictionary*)context;
[theData removeAllObjects];
}
在这个清理函数中,使用
__bridge_transfer
关键字将对象的内存管理从全局池转移到函数中。
4.8 向队列添加任务
向队列添加任务有两种方式:
- 同步方式:队列会等待任务完成。
- 异步方式:任务被添加后,函数会立即返回,不会等待任务完成。通常建议使用异步方式,因为它不会阻塞其他代码的运行。
可以将块或函数提交给队列执行,GCD 提供了四种调度函数:同步块、同步函数、异步块、异步函数。需要注意的是,为了避免死锁,不要在同一个队列上运行的任务中调用
dispatch_sync
或
dispatch_sync_f
。
下面是一个简单的 mermaid 流程图,展示了向队列添加任务的流程:
graph TD;
A[开始] --> B{选择添加任务方式};
B -->|同步| C[提交任务并等待完成];
B -->|异步| D[提交任务后立即返回];
C --> E[任务完成];
D --> E;
E --> F[结束];
综上所述,并发编程和块的使用是现代编程中非常重要的技术,通过合理运用 GCD 和调度队列,可以充分发挥多核处理器的优势,提高程序的性能和响应速度。同时,在使用块和处理并发时,要特别注意内存管理和同步问题,以确保程序的正确性和稳定性。
5. 调度队列的深入应用
5.1 串行队列的高级应用
5.1.1 任务顺序控制
串行队列的核心优势在于能够严格控制任务的执行顺序。例如,在一个数据处理程序中,可能需要先读取数据,然后对数据进行解析,最后将解析结果保存。可以使用串行队列确保这些任务按顺序依次执行。
dispatch_queue_t dataProcessingQueue = dispatch_queue_create("com.example.dataProcessingQueue", NULL);
dispatch_async(dataProcessingQueue, ^{
// 读取数据
NSLog(@"Reading data...");
});
dispatch_async(dataProcessingQueue, ^{
// 解析数据
NSLog(@"Parsing data...");
});
dispatch_async(dataProcessingQueue, ^{
// 保存数据
NSLog(@"Saving data...");
});
在这个示例中,三个任务会依次执行,不会出现数据解析在数据读取之前完成的情况。
5.1.2 避免资源竞争
当多个任务需要访问共享资源时,串行队列可以避免资源竞争问题。例如,多个任务可能需要修改同一个文件,使用串行队列可以确保每次只有一个任务在修改文件,从而避免数据冲突。
dispatch_queue_t fileModificationQueue = dispatch_queue_create("com.example.fileModificationQueue", NULL);
dispatch_async(fileModificationQueue, ^{
// 修改文件
NSLog(@"Modifying file...");
});
dispatch_async(fileModificationQueue, ^{
// 再次修改文件
NSLog(@"Modifying file again...");
});
5.2 并发队列的高级应用
5.2.1 并行任务处理
并发队列适合处理可以并行执行的任务,以提高程序的性能。例如,在一个图像处理应用中,需要对多张图片进行压缩处理,可以将每张图片的压缩任务分配到并发队列中。
dispatch_queue_t imageCompressionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSArray *imagePaths = @[@"image1.jpg", @"image2.jpg", @"image3.jpg"];
for (NSString *imagePath in imagePaths) {
dispatch_async(imageCompressionQueue, ^{
// 压缩图片
NSLog(@"Compressing image: %@", imagePath);
});
}
在这个示例中,三张图片的压缩任务会并行执行,大大缩短了整体处理时间。
5.2.2 任务依赖管理
虽然并发队列中的任务可以并行执行,但有时任务之间可能存在一定的依赖关系。可以使用
dispatch_group
来管理任务的依赖关系。
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
// 任务 1
NSLog(@"Task 1");
});
dispatch_group_async(group, concurrentQueue, ^{
// 任务 2
NSLog(@"Task 2");
});
dispatch_group_notify(group, concurrentQueue, ^{
// 任务 1 和任务 2 完成后执行的任务
NSLog(@"All tasks are completed");
});
在这个示例中,
dispatch_group_notify
会在
group
中的所有任务完成后执行。
5.3 主队列的高级应用
5.3.1 界面更新
主队列通常用于更新应用程序的用户界面,因为界面更新操作必须在主线程上执行。例如,在一个网络请求完成后,需要更新界面上的某个标签。
// 模拟网络请求
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 模拟网络请求耗时操作
sleep(2);
// 在主队列上更新界面
dispatch_async(dispatch_get_main_queue(), ^{
// 更新界面标签
NSLog(@"Updating UI label...");
});
});
在这个示例中,网络请求在全局并发队列中执行,而界面更新操作在主队列中执行,确保了界面更新的安全性。
5.3.2 多任务同步
主队列还可以用于多任务的同步操作。例如,在多个异步任务完成后,需要在主队列上执行一个汇总操作。
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
// 任务 1
NSLog(@"Task 1");
});
dispatch_group_async(group, concurrentQueue, ^{
// 任务 2
NSLog(@"Task 2");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 所有任务完成后在主队列上执行汇总操作
NSLog(@"Performing summary operation on main queue...");
});
6. 内存管理与上下文的进一步探讨
6.1 队列上下文的内存管理细节
在为队列分配上下文时,需要特别注意内存管理。当队列被释放时,上下文数据也需要被正确释放。可以结合清理函数来完成这个任务。
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity:5];
[myContext setObject:@"My Context" forKey:@"title"];
[myContext setObject:[NSNumber numberWithInt:0] forKey:@"value"];
dispatch_queue_t myQueue = dispatch_queue_create("com.example.myQueue", NULL);
dispatch_set_context(myQueue, (__bridge_retained void *)myContext);
void myFinalizerFunction(void *context) {
NSLog(@"Releasing context...");
NSMutableDictionary *theData = (__bridge_transfer NSMutableDictionary*)context;
[theData removeAllObjects];
[theData release];
}
dispatch_set_finalizer_f(myQueue, myFinalizerFunction);
// 释放队列
dispatch_release(myQueue);
在这个示例中,当队列被释放时,清理函数
myFinalizerFunction
会被调用,从而正确释放上下文数据。
6.2 块的内存管理最佳实践
在使用块时,要遵循一些内存管理的最佳实践,以避免内存泄漏。
- 避免循环引用:当块捕获
self
时,要确保不会形成循环引用。可以使用
__weak
修饰符来避免。
__weak typeof(self) weakSelf = self;
void (^myBlock)(void) = ^{
[weakSelf doSomething];
};
-
及时释放块:当块不再使用时,要及时释放。在 C 级别,可以使用
Block_release函数。
7. 并发编程的性能优化
7.1 合理选择队列类型
根据任务的特点选择合适的队列类型是提高并发编程性能的关键。
- 对于有严格顺序要求的任务,选择串行队列。
- 对于可以并行执行的任务,选择并发队列。
- 对于界面更新等必须在主线程执行的任务,选择主队列。
7.2 控制并发任务数量
虽然并发队列可以同时执行多个任务,但过多的并发任务可能会导致系统资源耗尽,反而降低性能。可以使用信号量来控制并发任务的数量。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); // 最多允许 2 个并发任务
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSArray *tasks = @[@"Task 1", @"Task 2", @"Task 3", @"Task 4"];
for (NSString *task in tasks) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(concurrentQueue, ^{
NSLog(@"Executing task: %@", task);
sleep(1); // 模拟任务耗时
dispatch_semaphore_signal(semaphore);
});
}
在这个示例中,使用信号量
semaphore
控制了并发任务的数量,最多允许 2 个任务同时执行。
7.3 减少锁的使用
锁是一种常用的同步机制,但锁的使用会带来一定的开销,可能会影响性能。在并发编程中,尽量减少锁的使用,可以使用无锁算法或其他同步机制来替代。
下面是一个 mermaid 流程图,展示了并发编程性能优化的流程:
graph TD;
A[开始] --> B{选择合适的队列类型};
B -->|串行队列| C[执行顺序任务];
B -->|并发队列| D{控制并发任务数量};
D -->|使用信号量| E[执行并行任务];
B -->|主队列| F[执行界面更新等任务];
C --> G{是否需要同步};
D --> G;
F --> G;
G -->|是| H[选择合适的同步机制];
G -->|否| I[继续执行任务];
H --> I;
I --> J{是否使用锁};
J -->|是| K[考虑减少锁的使用];
J -->|否| L[任务完成];
K --> L;
L --> M[结束];
8. 总结
并发编程和块的使用为现代编程带来了强大的功能和性能提升。通过合理运用 GCD 和调度队列,可以充分发挥多核处理器的优势,实现高效的任务处理。同时,在使用块和处理并发时,要特别注意内存管理和同步问题,遵循最佳实践,以确保程序的正确性和稳定性。在实际开发中,要根据任务的特点选择合适的队列类型和同步机制,控制并发任务数量,减少锁的使用,从而优化程序的性能。
通过本文的介绍,希望读者能够对并发编程和块的使用有更深入的理解,并在实际项目中灵活运用这些技术。
超级会员免费看
84万+

被折叠的 条评论
为什么被折叠?



