iOS底层原理系列04-并发编程

在移动应用开发中,流畅的用户体验至关重要,而并发编程是实现这一目标的关键技术。本文将深入探讨iOS平台上的并发编程和多线程架构,帮助你构建高性能、响应迅速的应用程序。

1. iOS线程调度机制

1.1 线程本质和iOS线程调度机制

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一个进程可以拥有多个线程,每个线程共享进程的资源,但拥有自己的执行路径。

在iOS系统中,线程调度由系统内核负责,通过优先级队列和时间片轮转算法确定线程的执行顺序和执行时长。苹果对标准的线程模型进行了优化,引入了更高级的抽象,如GCD和Operation,使开发者能够专注于任务本身,而非线程管理的细节。

1.2 并发计算模型中的同步/异步与串行/并行

并发编程中的两组基本概念:同步/异步和串行/并行。这两组概念看似简单,但常被混淆,它们实际上描述了两个不同的维度。

同步/异步的本质

同步(Synchronous)与异步(Asynchronous)描述的是调用方式 ,关注的是线程的等待方式

  • 同步调用:调用者会一直等待被调用的任务完成后,才继续执行后续代码。调用者线程在任务执行期间处于阻塞状态。
  • 异步调用:调用者不会等待被调用的任务完成,会立即继续执行后续代码。任务的完成通常通过回调、通知或其他机制通知调用者。

串行/并行的本质

**串行(Serial)与并行(Concurrent)**描述的是任务的执行方式,关注的是多个任务之间的关系。

  • 串行执行:多个任务按顺序依次执行,任何时刻只有一个任务在执行。
  • 并行执行:多个任务可以同时执行,任务之间相互独立,各自在不同的线程上执行。

同步/异步与串行/并行排列组合的调度机制和执行效果

这两组概念可以组合出四种不同的调度情况,下面我们详细分析每种组合的调度机制和执行效果。

1. 同步 + 串行

  • 调度机制:任务在当前线程上按顺序执行
  • 执行效果:调用者线程被阻塞,直到所有任务完成

2. 同步 + 并行

  • 调度机制:任务被分配到多个线程并发执行
  • 执行效果:调用者线程仍然被阻塞,直到所有任务完成
  • 注意:这种组合在实际中较少使用,因为即使任务并行执行,调用者仍需等待所有任务完成,无法充分利用并行的优势

3. 异步 + 串行

  • 调度机制:任务在另一个线程上按顺序执行
  • 执行效果:调用者线程立即返回继续执行后续代码,不会被阻塞

4. 异步 + 并行

  • 调度机制:任务被分配到多个线程并发执行
  • 执行效果:调用者线程立即返回继续执行后续代码,不会被阻塞,同时多个任务可以同时执行,充分利用多核处理器性能

这四种组合方式构成了iOS多线程编程的基础模型,也是理解GCD和NSOperation等高级API的关键。

2. iOS线程方案

iOS提供了多种多线程编程方案,从底层的pthread到高级的GCD和NSOperation,为开发者提供了灵活的选择。

2.1 pthread

pthread(POSIX thread)是一套跨平台的线程API标准,iOS也支持这一标准。它比NSThread更底层,提供了更多的控制选项。

objc 复制代码
#import <pthread.h>

// 创建线程
pthread_t thread;
pthread_create(&thread, NULL, ThreadFunction, NULL);

// 线程函数
void *ThreadFunction(void *data) {
    // 执行任务
    NSLog(@"任务在pthread中执行");
    return NULL;
}

// 等待线程结束
pthread_join(thread, NULL);

优点

  • 跨平台兼容性好
  • 控制粒度更细
  • 可以设置线程的各种属性

缺点

  • API复杂,使用不便
  • 需要手动管理线程的各个方面
  • 缺乏Objective-C与iOS集成的便利特性

2.2 NSThread

NSThread是Objective-C中最基本的线程类,它是对pthread的面向对象封装,提供了创建和管理线程的基本功能。

objc 复制代码
// 创建并启动线程
NSThread *thread = [[NSThread alloc] initWithTarget:self 
                                           selector:@selector(doSomething:) 
                                             object:nil];
thread.name = @"MyCustomThread";
[thread start];

// 或者使用类方法创建并自动启动线程
[NSThread detachNewThreadSelector:@selector(doSomething:) 
                         toTarget:self 
                       withObject:nil];

优点

  • 简单直观,面向对象的API
  • 可以直接控制线程的生命周期

缺点

  • 需要手动管理线程生命周期
  • 缺乏高级特性,如线程池、任务依赖等
  • 线程创建和销毁的开销较大

2.3 GCD

GCD是一个强大的并发编程框架,通过任务和队列的概念简化了多线程编程。GCD的核心思想是让开发者关注"做什么"而不是"怎么做"。

2.3.1 GCD的核心概念:

队列(Queue):负责存储和管理任务。

  • 串行队列(Serial Queue):按顺序执行任务。
  • 并行队列(Concurrent Queue):可以同时执行多个任务。
  • 主队列(Main Queue):在主线程上执行任务,通常用于UI更新。

任务(Task):以Block(代码块)形式提交到队列。

调度方式

  • 同步调度(sync):等待任务完成后返回。
  • 异步调度(async):提交任务后立即返回。

下面是GCD的常见用法示例:

objc 复制代码
// 获取全局并行队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 异步执行任务
dispatch_async(globalQueue, ^{
    // 耗时操作
    NSData *data = [self fetchDataFromServer];
    
    // 在主队列更新UI
    dispatch_async(dispatch_get_main_queue(), ^{
        [self updateUIWithData:data];
    });
});

// 创建自定义串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);

// 同步执行任务
dispatch_sync(serialQueue, ^{
    // 这会阻塞当前线程直到该任务完成
    [self processData];
});

2.3.2 GCD其他高级功能

如分组(group)、信号量(semaphore)、一次性执行(once)等:

objc 复制代码
// 使用组管理多个任务
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 添加任务到组
dispatch_group_async(group, queue, ^{
    // 任务1
});
dispatch_group_async(group, queue, ^{
    // 任务2
});

// 等待所有任务完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"所有任务已完成");
});

优点

  • 简洁高效的API
  • 自动管理线程池
  • 针对多核处理器优化
  • 低系统开销

缺点

  • 相比NSOperation,不支持取消任务
  • 不支持任务优先级(旧版本)
  • 调试难度相对较高

2.4 NSOperation/NSOperationQueue

NSOperationNSOperationQueue是基于GCD构建的更高级的抽象,提供了面向对象的API和更强大的任务管理能力。

NSOperation是一个抽象类,开发者通常使用其子类:

  1. NSBlockOperation:用于执行一个或多个Block的操作。
  2. NSInvocationOperation:用于调用特定对象的选择器。
  3. 自定义NSOperation子类:实现复杂任务逻辑。

NSOperationQueue用于管理和执行NSOperation对象:

objc 复制代码
// 创建队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // 设置最大并发数

// 创建操作
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSData *imageData = [self downloadImageData];
    UIImage *image = [UIImage imageWithData:imageData];
    
    // 在主队列更新UI
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.imageView.image = image;
    }];
}];

// 添加完成回调
operation1.completionBlock = ^{
    NSLog(@"图片下载完成");
};

// 添加操作到队列
[queue addOperation:operation1];

NSOperation/NSOperationQueue最强大的特性是可以设置操作之间的依赖关系,构建复杂的工作流:

objc 复制代码
// 创建多个操作
NSBlockOperation *downloadOp = [NSBlockOperation blockOperationWithBlock:^{
    // 下载图片
}];

NSBlockOperation *filterOp = [NSBlockOperation blockOperationWithBlock:^{
    // 过滤图片
}];

NSBlockOperation *saveOp = [NSBlockOperation blockOperationWithBlock:^{
    // 保存图片
}];

// 设置依赖关系
[filterOp addDependency:downloadOp]; // 先下载,再过滤
[saveOp addDependency:filterOp];     // 先过滤,再保存

// 添加到队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[downloadOp, filterOp, saveOp] waitUntilFinished:NO];

NSOperation还支持任务的取消、暂停和恢复:

objc 复制代码
// 取消单个操作
[operation cancel];

// 取消队列中所有操作
[queue cancelAllOperations];

// 暂停队列
queue.suspended = YES;

// 恢复队列
queue.suspended = NO;

优点

  • 面向对象的API
  • 支持操作的取消、暂停和恢复
  • 支持操作优先级
  • 支持操作间依赖关系
  • 内置了完成块(completionBlock)
  • KVO兼容,可以观察操作状态

缺点

  • 相比GCD,开销略大
  • API相对复杂
  • 初始化和配置需要更多代码

3. iOS中的线程安全方案

多线程编程中,一个关键问题是如何确保共享资源的访问安全。iOS提供了多种锁机制和同步方案,下面按性能从高到低介绍。

3.1 os_unfair_lock

这是iOS 10引入的锁机制,用于替代已废弃的OSSpinLock。它是一种低级互斥锁,性能极高。

objc 复制代码
#import <os/lock.h>

// 创建锁
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;

// 加锁
os_unfair_lock_lock(&lock);
// 临界区代码
os_unfair_lock_unlock(&lock);

// 尝试加锁(非阻塞)
if (os_unfair_lock_trylock(&lock)) {
    // 加锁成功,执行临界区代码
    os_unfair_lock_unlock(&lock);
} else {
    // 加锁失败
}

适用场景:需要高性能且临界区操作简短的场景。

3.2 OSSpinLock (已废弃)

自旋锁在等待锁释放时会持续尝试获取锁,不会进入休眠状态。虽然性能很高,但在iOS平台上存在优先级反转问题,已被苹果废弃。

objc 复制代码
#import <libkern/OSAtomic.h>

// 创建锁
OSSpinLock lock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&lock);
// 临界区代码
OSSpinLockUnlock(&lock);

注意:由于存在优先级反转问题,不推荐使用,应改用os_unfair_lock。

3.3 dispatch_semaphore_t

信号量是一种计数器,可以用来控制访问共享资源的线程数量。

objc 复制代码
// 创建信号量,初始值为1(表示互斥锁)
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

// 等待(减1,如果结果小于0则等待)
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 临界区代码
// 释放(加1,如果之前小于0则唤醒等待线程)
dispatch_semaphore_signal(semaphore);

适用场景:需要控制并发访问数量的场景,不仅限于互斥访问。

3.4 pthread_mutex

POSIX线程库提供的互斥锁,是一种通用的同步机制。

objc 复制代码
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

// 加锁
pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);

// 释放锁
pthread_mutex_destroy(&mutex);

适用场景:需要可靠互斥且对性能要求不极端高的通用场景。

3.5 dispatch_queue(DISPATCH_QUEUE_SERIAL)

串行队列可以用作同步机制,确保任务按顺序执行。

objc 复制代码
// 创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.example.safeQueue", DISPATCH_QUEUE_SERIAL);

// 同步执行(类似于加锁)
dispatch_sync(queue, ^{
    // 临界区代码
});

// 异步执行(非阻塞)
dispatch_async(queue, ^{
    // 临界区代码
});

适用场景:适合需要异步执行且保证顺序的场景,更偏向任务编排而非简单锁定。

3.6 NSLock

Foundation框架提供的Objective-C互斥锁类。

objc 复制代码
NSLock *lock = [[NSLock alloc] init];

// 加锁
[lock lock];
// 临界区代码
[lock unlock];

// 尝试加锁(非阻塞)
if ([lock tryLock]) {
    // 加锁成功
    [lock unlock];
}

// 带超时的加锁
if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]) {
    // 在1秒内获取到锁
    [lock unlock];
}

适用场景:需要面向对象API和超时功能的一般场景。

3.7 NSCondition

条件变量和互斥锁的结合,用于线程间的等待和通知机制。

objc 复制代码
NSCondition *condition = [[NSCondition alloc] init];

// 生产者线程
[condition lock];
// 修改共享数据
[condition signal]; // 或 [condition broadcast]
[condition unlock];

// 消费者线程
[condition lock];
while (/* 条件不满足 */) {
    [condition wait]; // 等待通知
}
// 临界区代码
[condition unlock];

适用场景:生产者-消费者模式等需要线程间通信的场景。

3.8 NSRecursiveLock

递归锁允许同一线程多次获取锁,而不会导致死锁。

objc 复制代码
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

// 第一次加锁
[lock lock];
// 可以再次获取同一把锁而不会死锁
[lock lock];
// 临界区代码
[lock unlock];
[lock unlock]; // 需要平衡调用unlock

适用场景:需要在递归调用或嵌套调用中使用锁的场景。

3.9 @synchronized

Objective-C提供的语言级同步原语,使用简单但性能相对较低。

objc 复制代码
@synchronized(self) {
    // 临界区代码
}

适用场景:对性能要求不高的简单同步场景,或原型开发。

3.10 原子属性 (atomic) 实现原理

Objective-C中的属性可以声明为atomic,保证读写操作的原子性:

objc 复制代码
@property (atomic, strong) NSString *name;

实现原理:编译器会为atomic属性生成访问器方法,使用自旋锁/互斥锁确保读写操作的原子性。

限制:atomic只保证单个属性的读写原子性,不保证相关操作的原子性。例如,对数组的原子性读写不保证数组内容的访问也是原子的。

3.11 读写安全方案

除了互斥锁外,还有专门针对读多写少场景优化的读写锁:

pthread_rwlock

读写锁允许多个线程同时读取共享资源,但写操作需要独占访问。

objc 复制代码
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

// 读锁(共享)
pthread_rwlock_rdlock(&rwlock);
// 读取操作
pthread_rwlock_unlock(&rwlock);

// 写锁(独占)
pthread_rwlock_wrlock(&rwlock);
// 写入操作
pthread_rwlock_unlock(&rwlock);

// 销毁锁
pthread_rwlock_destroy(&rwlock);
dispatch_barrier_async:异步栅栏调用

GCD提供的栅栏函数可以用于实现高效的读写分离:

objc 复制代码
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("com.example.rwQueue", DISPATCH_QUEUE_CONCURRENT);

// 读操作(多个可并发执行)
dispatch_async(queue, ^{
    // 读取操作
});

// 写操作(栅栏函数,保证独占访问)
dispatch_barrier_async(queue, ^{
    // 写入操作
});

栅栏函数的工作原理是:

  1. 等待队列中已有的任务完成
  2. 独占式执行栅栏任务
  3. 栅栏任务完成后,后续的普通任务才能执行

这种方式非常适合读多写少的场景,能够提供极高的并发性能。

4. 常见陷阱

4.1 死锁情况及预防

死锁发生在两个或多个线程互相等待对方释放锁的情况。最常见的死锁场景:

objc 复制代码
// 主线程
dispatch_sync(dispatch_get_main_queue(), ^{
    // 这会导致死锁,因为主线程尝试同步等待主队列的任务,
    // 而主队列的任务必须等待主线程完成当前执行
});

预防措施

  1. 避免在持有锁时获取另一个锁
  2. 如需多个锁,按固定顺序获取
  3. 使用带超时的锁获取方式
  4. 避免在主线程上同步派发到主队列
  5. 使用GCD的dispatch_group或信号量来协调多个异步操作

4.2 优先级反转

当低优先级线程持有锁,高优先级线程等待该锁,而中优先级线程占用CPU时,高优先级线程会被无限期阻塞。

解决方案

  1. 使用优先级继承的锁机制
  2. 避免在临界区执行耗时操作
  3. 使用合适的队列优先级

4.3 主线程阻塞引起的UI卡顿

在主线程上执行耗时操作会导致UI无法响应,造成卡顿感:

objc 复制代码
// 错误示例: 主线程同步等待耗时操作
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://example.com/large-file.zip"]];

正确做法

  1. 将耗时操作移至后台线程
  2. 使用异步API而非同步API
  3. 合理分解大任务为小任务
  4. 使用Instruments等工具监测和优化主线程性能
objc 复制代码
// 正确示例: 异步执行耗时操作,完成后回到主线程更新UI
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://example.com/large-file.zip"]];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // 更新UI
        [self.imageView setImage:[UIImage imageWithData:data]];
    });
});

总结

iOS提供了多种并发编程和线程安全的机制,从底层的pthread到高级的GCD和NSOperation。选择合适的机制需要考虑以下因素:

  1. 性能需求:对于性能要求极高的场景,考虑os_unfair_lock或dispatch_semaphore;对于一般场景,NSLock和串行队列足够。
  2. 编程范式:如果偏好面向对象的API,选择NSOperation和NSLock系列;如果偏好函数式编程,选择GCD。
  3. 任务特性:如果需要复杂的任务依赖和取消机制,选择NSOperation;如果是简单的并发任务,GCD更简洁。
  4. 同步需求:读多写少场景推荐读写锁或栅栏函数;需要线程通信的场景适合条件变量。
相关推荐
ikmb2 小时前
mac用docker跑sql server
macos·docker·容器
Macdo_cn5 小时前
Microsoft Excel 2024 LTSC mac v16.95 表格处理软件 支持M、Intel芯片
microsoft·macos·excel
Ethan. L6 小时前
iOS 模块化架构设计:主流方案与实现详解
ios·架构
算力魔方AIPC6 小时前
在MAC mini4上安装Ollama、Chatbox及模型交互指南
macos
二流小码农6 小时前
鸿蒙开发:权限管理之权限声明
android·ios·harmonyos
水木姚姚7 小时前
iOS应用程序开发(图片处理器)
macos·ios·objective-c·xcode·图标
༺ཌༀ傲世万物ༀད༻7 小时前
KICK第五课:Mac 系统下安装 Xcode 或 Clang
ide·macos·xcode
小镇之王7 小时前
【解决】XCode不支持旧版本的iOS设备
ios·cocoa·xcode
MrZWCui11 小时前
iOS OC使用正则表达式去除特殊符号并加粗文本,适用于接入AI大模型的流模式数据的文字处理
学习·ios·正则表达式·objective-c·xcode