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

Objective-C多线程并发控制基础与原理

2023-01-177.9k 阅读

多线程概述

在现代应用程序开发中,多线程编程是一项至关重要的技术。随着计算机硬件逐渐向多核处理器发展,充分利用多核资源以提升应用程序性能和响应能力成为必然趋势。多线程允许程序在同一时间执行多个任务,使得应用程序能够更有效地利用系统资源,避免因单一任务长时间运行而导致的界面卡顿等问题。

进程与线程的关系

在深入探讨Objective - C的多线程并发控制之前,我们需要明确进程(Process)和线程(Thread)的概念。进程是程序的一次执行过程,它拥有自己独立的内存空间和系统资源,是操作系统进行资源分配和调度的基本单位。而线程则是进程中的一个执行单元,是操作系统进行调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。

例如,当我们打开一个浏览器应用程序,这就是一个进程。在这个浏览器进程中,可能同时存在多个线程,比如一个线程负责加载网页内容,一个线程负责处理用户的鼠标和键盘操作,另一个线程可能负责检查是否有新的消息通知等。通过多线程,浏览器可以同时执行这些不同的任务,提高用户体验。

Objective - C中的多线程技术

Objective - C提供了多种实现多线程编程的方式,主要包括以下几种:

NSThread

NSThread是Objective - C中最基础的多线程类,它允许开发者直接创建和管理线程。通过NSThread,我们可以手动控制线程的启动、暂停、停止等操作。

创建一个NSThread实例有两种常见方式:

方式一:使用initWithTarget:selector:object:方法

- (void)createThreadWithTarget {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];
    [thread start];
}

- (void)runThread {
    // 线程执行的任务代码
    NSLog(@"This is a thread running.");
}

在上述代码中,我们创建了一个NSThread实例,指定了目标对象为当前对象(self),选择器为runThread方法,该方法将在线程启动后执行。

方式二:使用类方法detachNewThreadSelector:toTarget:withObject:

- (void)detachNewThread {
    [NSThread detachNewThreadSelector:@selector(runThread) toTarget:self withObject:nil];
}

- (void)runThread {
    NSLog(@"This is another way to start a thread.");
}

这种方式更为简洁,直接创建并启动了一个新线程。

虽然NSThread提供了直接控制线程的能力,但在实际开发中,由于手动管理线程的生命周期比较繁琐,且容易出现内存管理等问题,所以通常较少直接使用NSThread来进行复杂的多线程编程。

NSOperationQueue与NSOperation

NSOperationQueue和NSOperation提供了一种更高级、更灵活的多线程编程方式。NSOperation是一个抽象类,它定义了一个异步执行的任务。我们通常使用它的子类,如NSInvocationOperation、NSBlockOperation或者自定义继承自NSOperation的子类来表示具体的任务。

NSOperationQueue是一个队列,用于管理NSOperation对象。它会自动将队列中的NSOperation对象安排到合适的线程中执行。

NSInvocationOperation示例

- (void)useInvocationOperation {
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(taskMethod) object:nil];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operation];
}

- (void)taskMethod {
    NSLog(@"NSInvocationOperation is running.");
}

在上述代码中,我们创建了一个NSInvocationOperation实例,指定了要执行的任务方法taskMethod。然后将这个操作添加到NSOperationQueue中,队列会自动安排该操作在一个线程中执行。

NSBlockOperation示例

- (void)useBlockOperation {
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSBlockOperation is running.");
    }];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operation];
}

NSBlockOperation允许我们通过block来定义任务,这种方式更加简洁直观。

自定义NSOperation子类示例

@interface MyOperation : NSOperation
@end

@implementation MyOperation
- (void)main {
    if (!self.isCancelled) {
        NSLog(@"Custom NSOperation is running.");
    }
}
@end

- (void)useCustomOperation {
    MyOperation *operation = [[MyOperation alloc] init];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:operation];
}

通过自定义NSOperation子类,我们可以在main方法中实现更复杂的任务逻辑,并且可以根据需要控制任务的取消等操作。

NSOperationQueue还支持设置最大并发数,这可以帮助我们控制同时执行的任务数量,避免资源过度消耗。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 3; // 设置最大并发数为3

GCD(Grand Central Dispatch)

GCD是苹果公司为多核处理器优化的一种基于队列的高效异步执行任务的技术。它大大简化了多线程编程,开发者只需要定义任务并将其提交到合适的队列中,GCD会自动管理线程的创建、调度和销毁。

GCD中的队列分为两种类型:串行队列(Serial Queue)和并发队列(Concurrent Queue)。此外,还有两个特殊的队列:主队列(Main Queue)和全局队列(Global Queue)。

主队列:主队列是与主线程相关联的队列,在主队列中执行的任务会在主线程中运行。由于主线程主要负责处理用户界面更新等操作,所以在主队列中执行的任务应该尽量简短,避免阻塞主线程导致界面卡顿。

dispatch_async(dispatch_get_main_queue(), ^{
    // 在主线程中执行的任务
    self.titleLabel.text = @"Updated on main thread";
});

上述代码使用dispatch_async函数将一个更新界面的任务提交到主队列中执行。

全局队列:全局队列是系统提供的并发队列,根据优先级不同分为四个级别:高、默认、低和后台。我们可以通过dispatch_get_global_queue函数获取不同优先级的全局队列。

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
    // 在全局队列中执行的任务
    NSLog(@"Task is running in global queue.");
});

在上述代码中,我们获取了默认优先级的全局队列,并将一个任务提交到该队列中异步执行。

自定义串行队列:我们可以使用dispatch_queue_create函数创建自定义的串行队列。串行队列中的任务会按照添加的顺序依次执行。

dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
    NSLog(@"First task in serial queue.");
});
dispatch_async(serialQueue, ^{
    NSLog(@"Second task in serial queue.");
});

在上述代码中,先添加的任务会先执行,后添加的任务会等待前一个任务完成后再执行。

自定义并发队列:同样使用dispatch_queue_create函数,但创建时指定为并发队列。并发队列中的任务会根据系统资源情况同时执行多个。

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    NSLog(@"First task in concurrent queue.");
});
dispatch_async(concurrentQueue, ^{
    NSLog(@"Second task in concurrent queue.");
});

在并发队列中,两个任务可能会同时执行(取决于系统资源和CPU核心数等因素)。

多线程并发控制的问题与解决方案

在多线程编程中,由于多个线程可能同时访问和修改共享资源,会引发一些问题,主要包括以下几种:

资源竞争(Race Condition)

当多个线程同时访问和修改共享资源时,可能会导致数据不一致的问题,这就是资源竞争。例如,多个线程同时对一个共享的计数器进行加一操作,如果没有适当的同步机制,最终的结果可能与预期不符。

// 共享计数器
NSInteger sharedCounter = 0;

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
        sharedCounter++;
    });
}

// 等待所有任务完成
dispatch_barrier_sync(concurrentQueue, ^{
    NSLog(@"Final counter value: %ld", (long)sharedCounter);
});

在上述代码中,如果不进行同步控制,由于多个线程同时对sharedCounter进行操作,最终输出的结果可能不是1000。

死锁(Deadlock)

死锁是指两个或多个线程相互等待对方释放资源,从而导致所有线程都无法继续执行的情况。例如,线程A持有资源1并等待资源2,而线程B持有资源2并等待资源1,这样就形成了死锁。

dispatch_queue_t queue1 = dispatch_queue_create("com.example.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.queue2", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue1, ^{
    dispatch_sync(queue2, ^{
        NSLog(@"Task in queue1 accessing queue2.");
    });
});

dispatch_async(queue2, ^{
    dispatch_sync(queue1, ^{
        NSLog(@"Task in queue2 accessing queue1.");
    });
});

在上述代码中,线程在queue1中尝试同步访问queue2,而线程在queue2中尝试同步访问queue1,很可能会导致死锁。

解决方案

为了解决多线程并发控制中的问题,我们需要使用同步机制。在Objective - C中,常见的同步机制包括以下几种:

互斥锁(Mutex):互斥锁是一种最基本的同步工具,它保证在同一时间只有一个线程能够访问共享资源。在Objective - C中,可以使用dispatch_semaphore来实现互斥锁的功能。

// 创建一个信号量,初始值为1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
        // 等待信号量
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        sharedCounter++;
        // 释放信号量
        dispatch_semaphore_signal(semaphore);
    });
}

// 等待所有任务完成
dispatch_barrier_sync(concurrentQueue, ^{
    NSLog(@"Final counter value with semaphore: %ld", (long)sharedCounter);
});

在上述代码中,通过dispatch_semaphore_wait等待信号量,当信号量的值为1时,线程可以获取信号量并继续执行,同时信号量的值减为0。其他线程在等待信号量时会被阻塞,直到信号量被dispatch_semaphore_signal释放(值加为1)。

条件锁(Condition Lock):条件锁用于在满足特定条件时才允许线程访问共享资源。在Objective - C中,可以使用NSConditionLock类来实现条件锁。

NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1];

dispatch_queue_t queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    [conditionLock lockWhenCondition:1];
    // 满足条件时执行的任务
    NSLog(@"Task executed with condition lock.");
    [conditionLock unlockWithCondition:2];
});

dispatch_async(queue, ^{
    [conditionLock lockWhenCondition:2];
    // 满足条件时执行的任务
    NSLog(@"Another task executed with condition lock.");
    [conditionLock unlock];
});

在上述代码中,NSConditionLock根据不同的条件值来控制线程的执行。只有当条件满足时,线程才能获取锁并执行任务。

自旋锁(Spin Lock):自旋锁适用于短时间内需要频繁获取锁的场景。当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,该线程不会进入睡眠状态,而是在原地不断尝试获取锁,直到锁可用。在Objective - C中,可以使用os_unfair_lock来实现自旋锁。

os_unfair_lock unfairLock = OS_UNFAIR_LOCK_INIT;

dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
        os_unfair_lock_lock(&unfairLock);
        sharedCounter++;
        os_unfair_lock_unlock(&unfairLock);
    });
}

// 等待所有任务完成
dispatch_barrier_sync(concurrentQueue, ^{
    NSLog(@"Final counter value with spin lock: %ld", (long)sharedCounter);
});

需要注意的是,自旋锁会消耗CPU资源,如果长时间持有自旋锁,可能会导致系统性能下降,所以自旋锁适用于锁的持有时间非常短的场景。

线程间通信

在多线程编程中,线程之间常常需要进行通信,以协调任务的执行或者共享数据。在Objective - C中,常见的线程间通信方式有以下几种:

使用通知(NSNotification)

NSNotification是一种观察者模式的实现,允许一个对象发布通知,其他感兴趣的对象可以注册接收这些通知。在多线程环境下,我们可以在一个线程中发布通知,在另一个线程中接收并处理通知。

发布通知

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 模拟一些任务执行
    [NSThread sleepForTimeInterval:2];
    NSDictionary *userInfo = @{@"key": @"value"};
    [[NSNotificationCenter defaultCenter] postNotificationName:@"MyNotification" object:nil userInfo:userInfo];
});

在上述代码中,我们在一个全局队列的线程中执行一些任务后,发布了一个名为MyNotification的通知,并附带了一些用户信息。

接收通知

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"MyNotification" object:nil];

- (void)handleNotification:(NSNotification *)notification {
    NSDictionary *userInfo = notification.userInfo;
    NSLog(@"Received notification with userInfo: %@", userInfo);
}

通过addObserver:selector:name:object:方法注册接收通知,并在handleNotification:方法中处理接收到的通知。

使用KVO(Key - Value Observing)

KVO是一种基于观察者模式的机制,它允许我们观察对象属性的变化。在多线程环境下,当一个线程修改了被观察对象的属性时,其他注册了观察的线程可以收到通知并进行相应处理。

注册观察

MyObject *myObject = [[MyObject alloc] init];
[myObject addObserver:self forKeyPath:@"myProperty" options:NSKeyValueObservingOptionNew context:nil];

在上述代码中,我们对myObjectmyProperty属性进行观察。

属性变化处理

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"myProperty"]) {
        id newValue = change[NSKeyValueChangeNewKey];
        NSLog(@"Property value changed to: %@", newValue);
    }
}

myObjectmyProperty属性值发生变化时,observeValueForKeyPath:ofObject:change:context:方法会被调用。

使用同步队列和信号量进行通信

我们可以利用同步队列和信号量来实现线程间的简单通信。例如,一个线程在完成某个任务后,通过信号量通知另一个线程继续执行。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

dispatch_queue_t queue1 = dispatch_queue_create("com.example.queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("com.example.queue2", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue1, ^{
    // 执行任务
    NSLog(@"Task in queue1 is running.");
    [NSThread sleepForTimeInterval:2];
    // 发送信号量
    dispatch_semaphore_signal(semaphore);
});

dispatch_async(queue2, ^{
    // 等待信号量
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"Task in queue2 is running after receiving signal.");
});

在上述代码中,queue1中的任务完成后发送信号量,queue2中的任务在接收到信号量后才开始执行。

多线程性能优化

在进行多线程编程时,性能优化是非常重要的。以下是一些常见的多线程性能优化方法:

减少锁的使用

锁虽然可以解决资源竞争问题,但锁的获取和释放会带来一定的开销。尽量减少锁的使用范围和持有时间,可以提高程序性能。例如,将一些不需要同步的操作移出锁的保护范围。

// 优化前
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        // 一些不需要同步的操作也在锁内
        NSInteger temp = 1 + 2;
        sharedCounter++;
        dispatch_semaphore_signal(semaphore);
    });
}

// 优化后
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000; i++) {
    dispatch_async(concurrentQueue, ^{
        // 不需要同步的操作移到锁外
        NSInteger temp = 1 + 2;
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        sharedCounter++;
        dispatch_semaphore_signal(semaphore);
    });
}

合理使用队列

根据任务的特性选择合适的队列。对于一些需要顺序执行的任务,使用串行队列;对于可以并发执行的任务,使用并发队列。同时,合理设置队列的最大并发数,避免过多的线程竞争资源。

避免不必要的线程创建

线程的创建和销毁都有一定的开销,尽量复用现有的线程而不是频繁创建新线程。例如,使用NSOperationQueue或GCD的队列机制,它们会自动管理线程的复用。

数据局部化

尽量将数据处理限制在单个线程内,减少线程间的数据共享和同步操作。如果可能,将数据复制到线程本地,在本地线程中进行处理,最后再将结果合并。

总结

Objective - C的多线程并发控制是一个复杂而又关键的领域。通过掌握NSThread、NSOperationQueue与NSOperation以及GCD等多线程技术,了解并解决多线程编程中资源竞争、死锁等问题,合理运用同步机制和线程间通信方式,以及进行性能优化,开发者可以编写出高效、稳定且响应迅速的应用程序。在实际开发中,需要根据具体的业务需求和场景选择合适的多线程技术和优化策略,以充分发挥多核处理器的优势,提升应用程序的性能和用户体验。