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

Objective-C中的多线程资源共享与锁机制

2021-04-294.4k 阅读

多线程资源共享的概念与问题

在Objective - C开发中,多线程编程是提高应用程序性能和响应性的重要手段。多线程允许程序同时执行多个任务,例如在一个线程中进行网络请求,而在另一个线程中更新用户界面。然而,当多个线程访问共享资源时,就会出现一系列问题。

共享资源可以是内存中的数据结构,如数组、字典,也可以是文件、数据库连接等外部资源。多个线程同时对共享资源进行读写操作,可能会导致数据不一致或程序崩溃。

例如,假设有两个线程 Thread AThread B,它们都要对一个共享的整数变量 counter 进行加一操作。

// 定义共享变量
NSInteger counter = 0;

// 线程A的执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (int i = 0; i < 1000; i++) {
        counter++;
    }
});

// 线程B的执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    for (int i = 0; i < 1000; i++) {
        counter++;
    }
});

理论上,如果没有并发问题,counter 最终的值应该是 2000。但在实际运行中,由于两个线程可能同时读取 counter 的值,然后各自进行加一操作,导致一些加一操作被覆盖,最终 counter 的值会小于 2000。这就是典型的资源竞争问题,也被称为竞态条件(Race Condition)。

锁机制的引入

为了解决多线程资源共享的问题,锁机制应运而生。锁就像是一把钥匙,同一时间只有一个线程能够拿到这把钥匙,从而访问共享资源。当一个线程持有锁时,其他线程必须等待,直到该线程释放锁。

在Objective - C中,有多种类型的锁可供使用,每种锁都有其特点和适用场景。

互斥锁(Mutex)

互斥锁(Mutual Exclusion Lock)是最基本的锁类型,它保证在同一时间只有一个线程能够进入临界区(访问共享资源的代码块)。

在Objective - C中,可以使用 pthread_mutex_t 来创建和使用互斥锁。

#import <pthread.h>

// 定义互斥锁
pthread_mutex_t mutex;

// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);

// 线程执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 加锁
    pthread_mutex_lock(&mutex);
    // 临界区,访问共享资源
    // 例如对共享变量counter进行操作
    counter++;
    // 解锁
    pthread_mutex_unlock(&mutex);
});

在上述代码中,通过 pthread_mutex_lock 函数加锁,进入临界区对共享变量 counter 进行操作,操作完成后通过 pthread_mutex_unlock 函数解锁。这样就确保了同一时间只有一个线程能够修改 counter,避免了竞态条件。

递归锁(Recursive Lock)

递归锁允许同一个线程多次获取锁而不会造成死锁。这在一个函数可能会递归调用自身并且需要访问共享资源的情况下非常有用。

在Objective - C中,可以使用 pthread_rwlock_t 来创建递归锁。

#import <pthread.h>

// 定义递归锁
pthread_rwlock_t recursiveLock;

// 初始化递归锁
pthread_rwlock_init(&recursiveLock, NULL);

// 递归函数
void recursiveFunction() {
    // 加锁
    pthread_rwlock_wrlock(&recursiveLock);
    // 临界区
    // 可能会递归调用自身
    if (someCondition) {
        recursiveFunction();
    }
    // 解锁
    pthread_rwlock_unlock(&recursiveLock);
}

在这个例子中,recursiveFunction 函数可能会递归调用自身。如果使用普通的互斥锁,当函数递归调用时,第二次尝试获取锁会导致死锁,因为锁已经被当前线程持有。而递归锁允许同一线程多次获取锁,确保函数能够正常递归执行。

读写锁(Read - Write Lock)

读写锁适用于共享资源读操作远多于写操作的场景。它区分了读操作和写操作,允许多个线程同时进行读操作,但在写操作时会独占锁,不允许其他线程进行读或写操作。

在Objective - C中,可以使用 pthread_rwlock_t 来实现读写锁。

#import <pthread.h>

// 定义读写锁
pthread_rwlock_t rwLock;

// 初始化读写锁
pthread_rwlock_init(&rwLock, NULL);

// 读线程执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 加读锁
    pthread_rwlock_rdlock(&rwLock);
    // 执行读操作,访问共享资源
    // 例如读取共享变量counter的值
    NSInteger value = counter;
    // 解锁
    pthread_rwlock_unlock(&rwLock);
});

// 写线程执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 加写锁
    pthread_rwlock_wrlock(&rwLock);
    // 执行写操作,修改共享资源
    counter++;
    // 解锁
    pthread_rwlock_unlock(&rwLock);
});

在上述代码中,读线程通过 pthread_rwlock_rdlock 加读锁,允许多个读线程同时访问共享资源。写线程通过 pthread_rwlock_wrlock 加写锁,在写操作时独占锁,确保数据一致性。

NSLock及其相关类

除了基于POSIX的锁,Objective - C还提供了一些面向对象的锁类,如 NSLockNSRecursiveLockNSConditionLock

NSLock

NSLock 是一个简单的互斥锁类,使用起来更加面向对象。

// 创建NSLock对象
NSLock *lock = [[NSLock alloc] init];

// 线程执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 加锁
    [lock lock];
    // 临界区,访问共享资源
    counter++;
    // 解锁
    [lock unlock];
});

NSLock 的使用方式与 pthread_mutex_t 类似,但通过Objective - C的对象语法,代码更加易读和面向对象。

NSRecursiveLock

NSRecursiveLock 是递归锁的Objective - C版本。

// 创建NSRecursiveLock对象
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];

// 递归函数
void recursiveFunction() {
    // 加锁
    [recursiveLock lock];
    // 临界区
    // 可能会递归调用自身
    if (someCondition) {
        recursiveFunction();
    }
    // 解锁
    [recursiveLock unlock];
}

pthread_rwlock_t 实现的递归锁相比,NSRecursiveLock 同样提供了递归锁的功能,并且使用Objective - C对象语法,更加符合Objective - C的编程习惯。

NSConditionLock

NSConditionLock 是一种条件锁,它允许线程在满足特定条件时获取锁。

// 创建NSConditionLock对象,初始条件为0
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:0];

// 线程A执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 等待条件为1时获取锁
    [conditionLock lockWhenCondition:1];
    // 临界区,访问共享资源
    // 例如执行一些依赖条件1的操作
    // 解锁
    [conditionLock unlock];
});

// 线程B执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 设置条件为1
    [conditionLock lock];
    [conditionLock unlockWithCondition:1];
});

在这个例子中,线程A等待条件为1时获取锁,而线程B负责将条件设置为1。这种机制在多个线程需要按照特定顺序或条件执行任务时非常有用。

信号量(Semaphore)

信号量是一种更通用的同步工具,它可以控制同时访问共享资源的线程数量。信号量有一个计数器,每次获取信号量时计数器减一,每次释放信号量时计数器加一。当计数器为0时,获取信号量的操作会阻塞,直到有其他线程释放信号量。

在Objective - C中,可以使用 dispatch_semaphore_t 来创建和使用信号量。

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

// 线程执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 获取信号量
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 临界区,访问共享资源
    counter++;
    // 释放信号量
    dispatch_semaphore_signal(semaphore);
});

在上述代码中,信号量初始值为1,相当于一个互斥锁。线程在进入临界区前获取信号量,操作完成后释放信号量。如果将信号量初始值设为大于1的值,就可以允许多个线程同时进入临界区,同时控制进入临界区的线程数量。

死锁问题

死锁是多线程编程中一个严重的问题,当两个或多个线程相互等待对方释放锁时,就会发生死锁,导致程序无法继续执行。

例如,假设有两个线程 Thread AThread B,以及两把锁 Lock 1Lock 2

// 定义两把锁
NSLock *lock1 = [[NSLock alloc] init];
NSLock *lock2 = [[NSLock alloc] init];

// 线程A执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock1 lock];
    // 执行一些操作
    [lock2 lock];
    // 临界区,访问共享资源
    [lock2 unlock];
    [lock1 unlock];
});

// 线程B执行代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [lock2 lock];
    // 执行一些操作
    [lock1 lock];
    // 临界区,访问共享资源
    [lock1 unlock];
    [lock2 unlock];
});

在这个例子中,Thread A 先获取 Lock 1,然后尝试获取 Lock 2,而 Thread B 先获取 Lock 2,然后尝试获取 Lock 1。如果 Thread A 获取 Lock 1 后,Thread B 获取 Lock 2,就会导致两个线程相互等待对方释放锁,从而发生死锁。

为了避免死锁,可以遵循以下原则:

  1. 按顺序加锁:所有线程按照相同的顺序获取锁,例如总是先获取 Lock 1,再获取 Lock 2
  2. 避免嵌套锁:尽量减少锁的嵌套使用,降低死锁发生的可能性。
  3. 使用超时机制:在获取锁时设置超时时间,如果在规定时间内无法获取锁,则放弃操作并释放已获取的锁。

原子属性与非原子属性

在Objective - C中,属性可以声明为 atomicnonatomicatomic 属性会自动为属性的访问器方法(gettersetter)添加锁机制,确保同一时间只有一个线程能够访问属性。

@interface MyClass : NSObject
@property (nonatomic, strong) NSString *nonatomicString;
@property (atomic, strong) NSString *atomicString;
@end

@implementation MyClass
@end

nonatomic 属性不提供自动的线程安全机制,访问速度更快,适用于单线程环境或对性能要求较高且不需要线程安全的场景。atomic 属性虽然提供了线程安全,但由于加锁操作会带来性能开销,在多线程频繁访问属性的情况下,可能会影响性能。

多线程资源共享与锁机制的最佳实践

  1. 尽量减少共享资源:通过将数据分离到不同的线程或使用局部变量,减少多个线程需要共享的资源数量,从而降低资源竞争的可能性。
  2. 合理选择锁类型:根据应用场景的特点,选择合适的锁类型。如果读操作远多于写操作,读写锁可能是一个好选择;如果是简单的互斥需求,互斥锁或 NSLock 就足够了。
  3. 锁的粒度:尽量缩小锁的作用范围,只在访问共享资源的临界区加锁,减少锁的持有时间,提高并发性能。
  4. 避免不必要的锁:对于一些不需要线程安全的操作,不要使用锁,以提高性能。
  5. 测试与调试:在多线程编程中,测试和调试是非常重要的。使用工具如 Instruments 中的 Thread Sanitizer 来检测潜在的竞态条件和死锁问题。

示例:多线程下载图片并显示

下面通过一个多线程下载图片并在界面上显示的示例,来展示多线程资源共享与锁机制的实际应用。

假设我们有一个图片下载管理器,负责从网络下载图片,并将下载后的图片缓存起来,供多个视图控制器使用。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ImageDownloadManager : NSObject

@property (nonatomic, strong) NSMutableDictionary<NSString *, UIImage *> *imageCache;
@property (nonatomic, strong) NSLock *cacheLock;

+ (instancetype)sharedManager;
- (UIImage *)downloadImageWithURL:(NSURL *)url;

@end

@implementation ImageDownloadManager

+ (instancetype)sharedManager {
    static ImageDownloadManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[ImageDownloadManager alloc] init];
        manager.imageCache = [NSMutableDictionary dictionary];
        manager.cacheLock = [[NSLock alloc] init];
    });
    return manager;
}

- (UIImage *)downloadImageWithURL:(NSURL *)url {
    UIImage *image = nil;
    // 先检查缓存
    [self.cacheLock lock];
    image = self.imageCache[url.absoluteString];
    [self.cacheLock unlock];
    if (image) {
        return image;
    }
    // 如果缓存中没有,开始下载
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSData *data = [NSData dataWithContentsOfURL:url];
        if (data) {
            image = [UIImage imageWithData:data];
            // 将下载的图片存入缓存
            [self.cacheLock lock];
            self.imageCache[url.absoluteString] = image;
            [self.cacheLock unlock];
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return image;
}

@end

在这个示例中,ImageDownloadManager 使用 NSLock 来保护图片缓存 imageCache,确保在多线程环境下对缓存的读写操作是安全的。downloadImageWithURL: 方法先检查缓存中是否有图片,如果有则直接返回;如果没有则在后台线程下载图片,并在下载完成后将图片存入缓存。

在视图控制器中使用这个下载管理器:

#import "ViewController.h"
#import "ImageDownloadManager.h"

@interface ViewController ()

@property (nonatomic, strong) UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    [self.view addSubview:self.imageView];
    
    NSURL *imageURL = [NSURL URLWithString:@"https://example.com/image.jpg"];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        UIImage *image = [[ImageDownloadManager sharedManager] downloadImageWithURL:imageURL];
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
    });
}

@end

在视图控制器中,通过 ImageDownloadManager 下载图片,并在主线程更新 UIImageView 显示图片。这样既保证了图片下载在后台线程进行,不会阻塞主线程,又通过锁机制确保了图片缓存的线程安全。

通过以上详细介绍和示例代码,希望能帮助开发者深入理解Objective - C中的多线程资源共享与锁机制,在实际开发中编写出高效、稳定的多线程应用程序。在处理多线程问题时,需要根据具体场景仔细选择合适的同步工具,并遵循最佳实践原则,以避免常见的并发问题,如竞态条件和死锁。同时,不断的测试和调试也是确保多线程程序正确性的关键步骤。无论是简单的互斥需求,还是复杂的读写操作场景,都可以通过合理运用锁机制和多线程编程技巧来实现高效的资源共享和并发控制。在现代移动应用开发中,多线程编程已成为不可或缺的一部分,掌握这些知识对于提升应用性能和用户体验具有重要意义。

例如,在一个社交媒体应用中,可能同时有多个线程在上传图片、下载新消息、更新用户动态等,这些操作可能会共享一些资源,如用户信息、缓存数据等。通过正确使用锁机制,可以确保这些操作不会相互干扰,保证数据的一致性和应用的稳定性。

又如,在一个地图应用中,地图数据的加载和更新可能在后台线程进行,同时用户在主线程进行地图缩放、平移等操作,这就需要合理的同步机制来确保地图数据在多线程环境下的正确访问和更新。

在实际项目中,还可能会遇到更复杂的场景,如分布式系统中的多线程资源共享,这可能需要结合网络通信和分布式锁等技术来实现。但无论场景多么复杂,基本的多线程资源共享和锁机制原理都是相通的,开发者需要根据具体需求灵活运用这些知识。

此外,随着硬件技术的发展,多核处理器的广泛应用使得多线程编程的优势更加明显。但同时也带来了更多的并发挑战,需要开发者更加深入地理解和掌握多线程编程技术,以充分利用多核处理器的性能优势。

在Objective - C开发中,除了使用锁机制,还可以结合其他技术来优化多线程编程,如Grand Central Dispatch(GCD)和Operation Queue。GCD提供了一种简单而高效的异步编程模型,通过队列和任务来管理多线程执行。Operation Queue则更加面向对象,允许开发者对任务进行优先级设置、依赖管理等。

例如,使用GCD的队列组(Dispatch Group)可以实现多个异步任务完成后执行一个回调:

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, ^{
    // 第一个异步任务
});

dispatch_group_async(group, queue, ^{
    // 第二个异步任务
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 所有任务完成后执行的回调
});

在这个例子中,通过 dispatch_group_create 创建一个队列组,然后使用 dispatch_group_async 将任务添加到队列组中,最后通过 dispatch_group_notify 设置所有任务完成后在主线程执行的回调。

在多线程编程中,还需要注意内存管理问题。由于多个线程可能同时访问和修改对象,不正确的内存管理可能会导致内存泄漏或悬空指针等问题。在Objective - C中,ARC(自动引用计数)在一定程度上简化了内存管理,但在多线程环境下,仍然需要开发者小心处理对象的生命周期。

例如,在一个多线程环境中,如果一个对象在一个线程中被释放,而另一个线程仍然持有该对象的引用并尝试访问,就会导致程序崩溃。可以通过使用 __weak__strong 等修饰符来确保对象的正确引用和释放。

__weak id weakObject;
__strong id strongObject;

// 在一个线程中创建对象
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    strongObject = [[SomeObject alloc] init];
    weakObject = strongObject;
});

// 在另一个线程中访问对象
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    if (weakObject) {
        // 访问对象
    }
});

在这个例子中,__weak 修饰的 weakObject 不会增加对象的引用计数,当 strongObject 释放后,weakObject 会自动被设置为 nil,避免了悬空指针的问题。

另外,在多线程编程中,日志记录和错误处理也非常重要。由于多线程执行的不确定性,准确的日志记录可以帮助开发者追踪问题发生的时间和线程上下文。同时,合理的错误处理机制可以确保在出现问题时程序不会崩溃,而是能够优雅地处理错误并提供给用户友好的提示。

例如,在下载图片的示例中,可以在下载失败时记录错误日志,并在主线程提示用户下载失败:

- (UIImage *)downloadImageWithURL:(NSURL *)url {
    UIImage *image = nil;
    [self.cacheLock lock];
    image = self.imageCache[url.absoluteString];
    [self.cacheLock unlock];
    if (image) {
        return image;
    }
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSError *error = nil;
        NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
        if (data) {
            image = [UIImage imageWithData:data];
            [self.cacheLock lock];
            self.imageCache[url.absoluteString] = image;
            [self.cacheLock unlock];
        } else {
            NSLog(@"Image download failed: %@", error);
            dispatch_async(dispatch_get_main_queue(), ^{
                // 提示用户下载失败
            });
        }
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return image;
}

在这个改进的代码中,通过 NSError 捕获下载过程中的错误,并在主线程提示用户下载失败,同时记录错误日志。

在多线程编程的学习和实践过程中,开发者还可以参考一些优秀的开源项目。许多开源项目都涉及到复杂的多线程编程,通过学习这些项目的代码结构和同步机制的运用,可以快速提升自己的多线程编程能力。

例如,AFNetworking是一个广泛使用的网络框架,它在处理网络请求和数据缓存等方面运用了多线程编程技术。通过研究AFNetworking的源码,可以学习到如何在多线程环境下高效地管理网络任务、处理数据共享和同步等问题。

总之,Objective - C中的多线程资源共享与锁机制是一个复杂而又关键的领域。开发者需要深入理解各种锁类型的特点和适用场景,掌握多线程编程的最佳实践,同时注意内存管理、日志记录和错误处理等方面。通过不断的学习和实践,结合实际项目需求,才能编写出高性能、稳定的多线程应用程序。无论是移动应用开发、桌面应用开发还是服务器端开发,多线程编程技术都有着广泛的应用前景,对于提升应用的性能和用户体验具有不可忽视的作用。