MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C多线程编程与GCD使用指南

2022-03-181.7k 阅读

多线程编程基础

在深入探讨Objective-C中的多线程编程与GCD之前,我们先来回顾一下多线程编程的一些基础知识。

1. 什么是线程

线程是程序执行流的最小单元,一个进程可以包含多个线程。每个线程都有自己独立的执行路径,共享进程的资源,如内存空间、文件描述符等。多线程编程允许程序同时执行多个任务,提高程序的响应性和效率。

在单线程程序中,任务按照顺序依次执行。例如,下面这段简单的Objective-C代码:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"开始执行任务1");
        for (int i = 0; i < 1000000; i++) {
            // 模拟一个耗时操作
        }
        NSLog(@"任务1执行完毕");

        NSLog(@"开始执行任务2");
        for (int j = 0; j < 1000000; j++) {
            // 模拟另一个耗时操作
        }
        NSLog(@"任务2执行完毕");
    }
    return 0;
}

在这个例子中,任务1和任务2是顺序执行的,只有当任务1完成后,任务2才会开始。如果任务1非常耗时,那么在任务1执行期间,程序将无法响应用户的其他操作,导致界面卡顿等问题。

而多线程编程可以解决这个问题,它允许我们将不同的任务分配到不同的线程中执行,从而实现并发执行。

2. 多线程的优势

  • 提高程序响应性:在图形用户界面(GUI)应用程序中,将耗时操作放在后台线程执行,可以避免主线程被阻塞,保证界面的流畅性和响应性。例如,在一个下载文件的应用中,如果下载操作在主线程执行,那么在下载过程中,用户将无法点击其他按钮或进行其他操作。而将下载操作放在后台线程,主线程仍然可以处理用户的交互事件。
  • 充分利用多核处理器:现代计算机通常配备多核处理器,多线程编程可以让不同的线程在不同的核心上并行执行,从而提高程序的整体性能。例如,在进行复杂的图像渲染任务时,可以将图像的不同部分分配到不同的线程进行渲染,利用多核处理器的并行计算能力,加快渲染速度。
  • 提高资源利用率:在某些情况下,一个线程可能会因为等待I/O操作完成而处于阻塞状态。此时,其他线程可以继续执行,从而充分利用CPU资源。比如在网络请求时,线程可能会等待数据从网络传输过来,这段时间内CPU处于空闲状态,如果有其他线程,就可以利用这段时间执行其他任务。

3. 多线程带来的问题

  • 资源竞争:由于多个线程共享进程的资源,当多个线程同时访问和修改同一个资源时,就可能会出现数据不一致的问题。例如,两个线程同时读取一个变量的值,然后分别对其进行加1操作,最后将结果写回变量。由于线程执行的顺序是不确定的,可能会导致最终变量的值并不是预期的加2的结果。
  • 死锁:死锁是指两个或多个线程相互等待对方释放资源,从而导致所有线程都无法继续执行的情况。例如,线程A持有资源1并请求资源2,而线程B持有资源2并请求资源1,这样两个线程就会互相等待,形成死锁。
  • 线程安全问题:除了资源竞争和死锁,多线程编程还可能面临其他线程安全问题,如竞态条件(Race Condition)等。这些问题都需要我们在编写多线程程序时特别注意,通过合适的同步机制来解决。

Objective-C中的多线程编程方式

Objective-C提供了多种方式来实现多线程编程,主要包括NSThread、NSOperationQueue和Grand Central Dispatch(GCD)。下面我们分别介绍这些方式。

1. NSThread

NSThread是Objective-C中最基础的多线程编程类,它允许我们直接创建和管理线程。

  • 创建和启动线程 可以通过以下两种方式创建NSThread实例:
// 方式一:直接创建并启动线程
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(taskMethod) object:nil];
[thread1 start];

// 方式二:使用类方法创建并启动线程
[NSThread detachNewThreadSelector:@selector(taskMethod) toTarget:self withObject:nil];

在上面的代码中,taskMethod是我们定义的在新线程中执行的方法,self是调用该方法的对象,nil是传递给方法的参数。

  • 线程属性和操作 NSThread提供了一些属性和方法来管理线程,例如:
// 获取当前线程
NSThread *currentThread = [NSThread currentThread];

// 设置线程名称
[currentThread setName:@"MyThread"];

// 暂停当前线程
[NSThread sleepForTimeInterval:2.0]; // 暂停2秒

// 强制终止线程(不推荐,可能导致资源泄漏等问题)
[thread1 cancel];
  • 线程同步 由于多个线程可能会同时访问共享资源,所以需要进行线程同步。NSThread可以使用锁(如NSLock、NSConditionLock等)来实现线程同步。
NSLock *lock = [[NSLock alloc] init];

- (void)taskMethod {
    [lock lock];
    // 访问共享资源的代码
    [lock unlock];
}

虽然NSThread提供了基本的多线程功能,但它的使用相对繁琐,需要手动管理线程的生命周期、同步等问题。在实际开发中,通常会使用更高级的多线程编程方式,如NSOperationQueue和GCD。

2. NSOperationQueue

NSOperationQueue是基于队列的异步执行任务的机制,它管理着一组NSOperation对象。NSOperation是一个抽象类,我们通常使用它的子类NSInvocationOperation或NSBlockOperation,或者自定义一个继承自NSOperation的子类。

  • NSInvocationOperation
// 创建一个NSInvocationOperation
NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskMethod) object:nil];
// 创建一个操作队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 将操作添加到队列中执行
[queue addOperation:operation1];
  • NSBlockOperation
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    // 执行的任务代码
}];
[queue addOperation:operation2];
  • 自定义NSOperation
@interface MyOperation : NSOperation
@end

@implementation MyOperation
- (void)main {
    if (![self isCancelled]) {
        // 执行任务的代码
    }
}
@end

MyOperation *customOperation = [[MyOperation alloc] init];
[queue addOperation:customOperation];
  • 操作依赖和优先级 NSOperationQueue支持操作之间的依赖关系和优先级设置。
// 设置操作依赖
[operation2 addDependency:operation1]; // operation2将在operation1完成后执行

// 设置操作优先级
operation1.queuePriority = NSOperationQueuePriorityHigh;

NSOperationQueue相对NSThread来说,使用起来更加方便和灵活,它自动管理线程的创建、调度和销毁,并且提供了操作依赖、优先级等功能,使得多线程编程更加容易控制。

Grand Central Dispatch(GCD)

Grand Central Dispatch(GCD)是Apple开发的一种基于队列的高效异步编程模型,它在iOS 4.0及以上版本可用。GCD极大地简化了多线程编程,让我们可以更轻松地实现并发和异步任务。

1. GCD的基本概念

  • 队列(Queue) GCD中的队列是用来存放任务(block)的,任务会按照先进先出(FIFO)的顺序从队列中取出并执行。GCD提供了两种类型的队列:串行队列(Serial Queue)和并发队列(Concurrent Queue)。
    • 串行队列:每次只有一个任务在执行,前一个任务完成后,下一个任务才会开始。可以通过dispatch_queue_create函数创建自定义的串行队列:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
- **并发队列**:可以同时执行多个任务,任务的执行顺序仍然是FIFO,但多个任务可以并行执行。系统提供了全局并发队列,可以通过`dispatch_get_global_queue`函数获取:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  • 任务(Task) GCD中的任务是用block来表示的,我们可以将需要执行的代码放在block中,然后将block提交到队列中执行。任务分为同步任务(Synchronous Task)和异步任务(Asynchronous Task)。
    • 同步任务:使用dispatch_sync函数提交,同步任务会阻塞当前线程,直到任务执行完毕。例如:
dispatch_sync(serialQueue, ^{
    // 同步任务代码
});
- **异步任务**:使用`dispatch_async`函数提交,异步任务不会阻塞当前线程,任务提交后会立即返回,继续执行后续代码。例如:
dispatch_async(serialQueue, ^{
    // 异步任务代码
});

2. GCD的使用示例

  • 在后台线程执行任务 假设我们有一个耗时的计算任务,为了不阻塞主线程,我们可以将其放在后台线程执行。
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 耗时计算任务
    long long result = 0;
    for (long long i = 0; i < 1000000000; i++) {
        result += i;
    }
    // 回到主线程更新UI
    dispatch_async(dispatch_get_main_queue(), ^{
        // 更新UI的代码
        NSLog(@"计算结果: %lld", result);
    });
});

在这个例子中,首先通过dispatch_get_global_queue获取全局并发队列,然后使用dispatch_async将耗时计算任务提交到该队列中异步执行。由于更新UI必须在主线程进行,所以计算完成后,使用dispatch_async将更新UI的任务提交到主线程队列(dispatch_get_main_queue获取)。

  • 同步任务和异步任务的区别 下面通过一个简单的示例来演示同步任务和异步任务的区别:
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);

NSLog(@"开始同步任务");
dispatch_sync(serialQueue, ^{
    for (int i = 0; i < 3; i++) {
        NSLog(@"同步任务中: %d", i);
        [NSThread sleepForTimeInterval:1.0];
    }
});
NSLog(@"同步任务结束");

NSLog(@"开始异步任务");
dispatch_async(serialQueue, ^{
    for (int j = 0; j < 3; j++) {
        NSLog(@"异步任务中: %d", j);
        [NSThread sleepForTimeInterval:1.0];
    }
});
NSLog(@"异步任务提交后立即执行这里");

运行这段代码,可以看到同步任务会阻塞当前线程,直到任务执行完毕,而异步任务不会阻塞当前线程,任务提交后会立即继续执行后续代码。

  • 使用并发队列实现并行任务 假设有多个独立的任务需要并行执行,可以使用并发队列:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i < 5; i++) {
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务 %d 开始", i);
        [NSThread sleepForTimeInterval:2.0];
        NSLog(@"任务 %d 结束", i);
    });
}

在这个例子中,5个任务被提交到并发队列中,它们会并行执行,提高了整体的执行效率。

3. GCD的高级特性

  • Dispatch Group Dispatch Group可以用来等待一组任务完成。例如,我们有多个网络请求任务,需要在所有请求都完成后进行一些汇总操作:
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_async(group, globalQueue, ^{
    // 第一个网络请求任务
});

dispatch_group_async(group, globalQueue, ^{
    // 第二个网络请求任务
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 所有任务完成后执行的代码,通常用于汇总结果等操作
    NSLog(@"所有网络请求完成");
});
  • Dispatch Semaphore Dispatch Semaphore可以用来控制同时访问资源的线程数量。例如,我们有一个资源只能同时被3个线程访问,可以这样实现:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);

for (int i = 0; i < 10; i++) {
    dispatch_async(globalQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        // 访问共享资源的代码
        NSLog(@"线程 %d 访问资源", i);
        [NSThread sleepForTimeInterval:1.0];
        dispatch_semaphore_signal(semaphore);
    });
}

在这个例子中,dispatch_semaphore_create(3)创建了一个信号量,初始值为3,表示最多允许3个线程同时访问资源。dispatch_semaphore_wait会等待信号量的值大于0,然后将信号量的值减1,允许线程访问资源。dispatch_semaphore_signal会将信号量的值加1,释放一个访问资源的名额。

  • Dispatch Barrier Dispatch Barrier可以在并发队列中插入一个同步点,确保在barrier之前的任务执行完毕后,才开始执行barrier之后的任务,并且barrier之后的任务会单独执行,不会与其他任务并行。例如,在一个数据库读写操作中,写操作需要独占资源,我们可以这样使用Dispatch Barrier:
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

// 多个读操作
for (int i = 0; i < 3; i++) {
    dispatch_async(concurrentQueue, ^{
        // 读操作代码
        NSLog(@"读操作 %d", i);
    });
}

// 写操作
dispatch_barrier_async(concurrentQueue, ^{
    // 写操作代码,会等待前面的读操作完成后单独执行
    NSLog(@"写操作");
});

// 后续读操作
for (int j = 0; j < 3; j++) {
    dispatch_async(concurrentQueue, ^{
        // 读操作代码
        NSLog(@"读操作 %d", j);
    });
}

在这个例子中,写操作通过dispatch_barrier_async提交到队列中,它会等待前面的读操作完成后再执行,并且不会与其他任务并行,保证了写操作的原子性。

多线程编程中的线程安全

在多线程编程中,线程安全是一个非常重要的问题。由于多个线程可能同时访问和修改共享资源,如全局变量、静态变量等,如果不进行适当的同步控制,就可能导致数据不一致、竞态条件等问题。

1. 同步机制

  • 锁(Lock) 锁是最常用的同步机制之一,它可以保证在同一时间只有一个线程能够访问被保护的资源。在Objective-C中,常用的锁有NSLock、NSRecursiveLock、NSConditionLock等。
NSLock *lock = [[NSLock alloc] init];

- (void)accessSharedResource {
    [lock lock];
    // 访问共享资源的代码
    [lock unlock];
}
  • 信号量(Semaphore) 信号量可以控制同时访问资源的线程数量。例如,上面介绍的Dispatch Semaphore就是GCD中提供的信号量机制。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_async(globalQueue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 访问共享资源的代码
    dispatch_semaphore_signal(semaphore);
});
  • 条件变量(Condition Variable) 条件变量可以让线程在满足特定条件时才继续执行。例如,NSCondition类提供了条件变量的功能。
NSCondition *condition = [[NSCondition alloc] init];
BOOL dataReady = NO;

- (void)producer {
    [condition lock];
    // 生成数据的代码
    dataReady = YES;
    [condition signal];
    [condition unlock];
}

- (void)consumer {
    [condition lock];
    while (!dataReady) {
        [condition wait];
    }
    // 消费数据的代码
    [condition unlock];
}

2. 原子性操作

除了使用同步机制,还可以使用原子性操作来保证数据的一致性。在Objective-C中,属性可以通过atomic关键字声明为原子属性,它会自动为属性的读写操作添加锁,保证在同一时间只有一个线程能够访问该属性。

@property (nonatomic, atomic, assign) int counter;

然而,需要注意的是,atomic属性只能保证单个属性的读写操作是线程安全的,但对于复杂的操作,如counter++,仍然需要额外的同步机制,因为counter++实际上包含了读、加、写三个操作,atomic无法保证这三个操作的原子性。

性能优化与注意事项

在进行多线程编程时,除了保证线程安全,还需要注意性能优化,以充分发挥多线程的优势。

1. 避免过度使用多线程

虽然多线程可以提高程序的性能,但创建和管理线程也会消耗系统资源。如果创建过多的线程,可能会导致系统性能下降,甚至出现内存溢出等问题。因此,在使用多线程时,需要根据实际情况合理控制线程的数量。

2. 减少锁的使用

锁虽然可以保证线程安全,但也会带来性能开销。每次加锁和解锁都会消耗一定的时间,并且锁会导致线程之间的竞争,降低并行性。因此,在设计多线程程序时,应该尽量减少锁的使用范围和时间,例如,只在访问共享资源的关键代码段加锁,而不是在整个方法中加锁。

3. 合理选择队列类型

在使用GCD时,要根据任务的特点合理选择队列类型。对于需要顺序执行的任务,使用串行队列;对于可以并行执行的任务,使用并发队列。同时,要注意并发队列中任务的数量和执行顺序,避免出现资源竞争和死锁等问题。

4. 注意内存管理

在多线程环境下,内存管理也变得更加复杂。由于多个线程可能同时访问和修改对象,需要注意对象的生命周期和内存释放。例如,在一个线程中释放了一个对象,而另一个线程可能还在使用该对象,就会导致野指针错误。可以使用自动引用计数(ARC)来简化内存管理,但仍然需要注意多线程环境下的内存访问问题。

通过合理运用多线程编程技术,结合GCD的高效特性,并注意线程安全和性能优化,我们可以编写出更加高效、稳定的Objective-C程序。无论是开发iOS应用还是其他基于Objective-C的软件,掌握多线程编程都是非常重要的技能。在实际开发中,需要根据具体的需求和场景,选择合适的多线程编程方式和同步机制,以实现最佳的性能和用户体验。同时,不断地实践和调试,积累经验,才能更好地应对多线程编程中可能遇到的各种问题。