26、深入理解块与并发编程

深入理解块与并发编程

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 和调度队列,可以充分发挥多核处理器的优势,实现高效的任务处理。同时,在使用块和处理并发时,要特别注意内存管理和同步问题,遵循最佳实践,以确保程序的正确性和稳定性。在实际开发中,要根据任务的特点选择合适的队列类型和同步机制,控制并发任务数量,减少锁的使用,从而优化程序的性能。

通过本文的介绍,希望读者能够对并发编程和块的使用有更深入的理解,并在实际项目中灵活运用这些技术。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值