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

Objective-C多线程中的数据同步与锁机制

2021-11-207.0k 阅读

多线程编程中的数据同步问题

在多线程编程中,数据同步是一个至关重要的问题。当多个线程同时访问和修改共享数据时,可能会导致数据不一致、竞争条件(race condition)等问题。例如,假设我们有一个简单的计数器变量 counter,有两个线程都要对它进行加 1 操作。如果没有适当的同步机制,可能会出现以下情况:

线程 1 读取 counter 的值,假设为 10。同时,线程 2 也读取了 counter 的值,也是 10。然后线程 1 将 counter 加 1 并写回,此时 counter 变为 11。接着线程 2 也将 counter 加 1 并写回,由于它最初读取的值是 10,所以写回后 counter 还是 11,而不是预期的 12。这就是一个典型的竞争条件,因为两个线程在没有协调的情况下同时访问和修改了共享数据。

在 Objective - C 多线程编程中,这种情况同样可能发生。比如我们在一个 iOS 应用程序中,多个视图控制器的线程可能需要访问和修改应用程序的全局数据,如用户的登录状态、购物车信息等。如果没有正确的数据同步,就可能导致应用程序出现不可预测的行为。

共享资源与竞争条件

共享资源是指多个线程可以同时访问的资源,如内存中的变量、文件、网络连接等。当多个线程对共享资源进行读写操作时,如果没有合适的同步机制,就会产生竞争条件。竞争条件会导致程序的行为变得不可预测,可能在某些情况下运行正常,但在其他情况下出现错误。

例如,考虑以下简单的 Objective - C 代码:

@interface Counter : NSObject
@property (nonatomic, assign) NSInteger value;
@end

@implementation Counter
@end

// 在某个视图控制器中
- (void)incrementCounter {
    Counter *counter = [[Counter alloc] init];
    counter.value = 0;

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

    dispatch_async(queue1, ^{
        for (NSInteger i = 0; i < 1000; i++) {
            counter.value++;
        }
    });

    dispatch_async(queue2, ^{
        for (NSInteger i = 0; i < 1000; i++) {
            counter.value++;
        }
    });

    // 等待两个任务完成
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue1, ^{
        for (NSInteger i = 0; i < 1000; i++) {
            counter.value++;
        }
    });
    dispatch_group_async(group, queue2, ^{
        for (NSInteger i = 0; i < 1000; i++) {
            counter.value++;
        }
    });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

    NSLog(@"Final counter value: %ld", (long)counter.value);
}

在上述代码中,我们创建了一个 Counter 类,有一个 value 属性。然后在一个方法中,我们创建了两个并发队列,并向队列中添加任务来对 counter.value 进行自增操作。理论上,如果没有竞争条件,最终 counter.value 的值应该是 2000(假设只启动两个线程且每个线程自增 1000 次)。但实际上,由于竞争条件的存在,每次运行结果可能都不一样,往往会小于 2000。

数据不一致的后果

数据不一致可能会导致程序出现各种错误,如计算结果错误、逻辑混乱、程序崩溃等。在一个金融应用程序中,如果多个线程同时处理账户余额的更新,数据不一致可能导致用户的账户余额显示错误,甚至可能导致资金的错误转移。在一个游戏开发中,如果多个线程同时处理游戏角色的状态更新,数据不一致可能导致角色的位置、生命值等信息出现混乱,严重影响游戏体验。

Objective - C 中的锁机制

为了解决多线程编程中的数据同步问题,Objective - C 提供了多种锁机制。锁机制的基本原理是,当一个线程获取到锁时,其他线程就不能再获取同一把锁,直到该线程释放锁。这样就保证了在同一时间只有一个线程能够访问共享资源,从而避免竞争条件。

NSLock

NSLock 是 Objective - C 中最基本的锁类。它提供了简单的加锁和解锁方法。

@interface MyClass : NSObject
@property (nonatomic, strong) NSLock *lock;
@property (nonatomic, assign) NSInteger sharedValue;
@end

@implementation MyClass
- (instancetype)init {
    self = [super init];
    if (self) {
        _lock = [[NSLock alloc] init];
        _sharedValue = 0;
    }
    return self;
}

- (void)incrementSharedValue {
    [self.lock lock];
    self.sharedValue++;
    [self.lock unlock];
}
@end

在上述代码中,MyClass 类有一个 NSLock 实例和一个共享变量 sharedValue。在 incrementSharedValue 方法中,我们首先调用 lock 方法获取锁,然后对 sharedValue 进行自增操作,最后调用 unlock 方法释放锁。这样,当一个线程在执行 incrementSharedValue 方法时,其他线程调用该方法就会被阻塞,直到当前线程释放锁。

NSRecursiveLock

NSRecursiveLock 是一种递归锁。递归锁允许同一个线程多次获取同一把锁而不会造成死锁。这在一些递归函数或者需要多次嵌套调用加锁方法的场景中非常有用。

@interface RecursiveClass : NSObject
@property (nonatomic, strong) NSRecursiveLock *recursiveLock;
@property (nonatomic, assign) NSInteger recursiveValue;
@end

@implementation RecursiveClass
- (instancetype)init {
    self = [super init];
    if (self) {
        _recursiveLock = [[NSRecursiveLock alloc] init];
        _recursiveValue = 0;
    }
    return self;
}

- (void)recursiveIncrement:(NSInteger)count {
    [self.recursiveLock lock];
    if (count > 0) {
        self.recursiveValue++;
        [self recursiveIncrement:count - 1];
    }
    [self.recursiveLock unlock];
}
@end

在上述代码中,RecursiveClass 类有一个 NSRecursiveLock 实例和一个 recursiveValue 变量。recursiveIncrement: 方法是一个递归方法,每次调用都会获取锁。如果使用普通的 NSLock,由于同一个线程多次获取锁会导致死锁,而 NSRecursiveLock 则可以避免这种情况。

NSConditionLock

NSConditionLock 是一种条件锁。它允许线程在满足特定条件时获取锁。这在一些需要根据不同条件进行同步的场景中很有用。

@interface ConditionClass : NSObject
@property (nonatomic, strong) NSConditionLock *conditionLock;
@property (nonatomic, assign) NSInteger conditionValue;
@end

@implementation ConditionClass
- (instancetype)init {
    self = [super init];
    if (self) {
        _conditionLock = [[NSConditionLock alloc] initWithCondition:0];
        _conditionValue = 0;
    }
    return self;
}

- (void)updateValueWithCondition:(NSInteger)condition {
    [self.conditionLock lockWhenCondition:condition];
    self.conditionValue++;
    [self.conditionLock unlockWithCondition:condition + 1];
}
@end

在上述代码中,ConditionClass 类有一个 NSConditionLock 实例和一个 conditionValue 变量。updateValueWithCondition: 方法在调用 lockWhenCondition: 时,只有当锁的当前条件与传入的 condition 相匹配时,线程才能获取锁。然后对 conditionValue 进行更新,并通过 unlockWithCondition: 方法释放锁并设置新的条件。

@synchronized 关键字

@synchronized 是 Objective - C 提供的一种便捷的同步方式。它不需要手动创建锁对象,而是基于对象本身进行同步。

@interface SynchronizedClass : NSObject
@property (nonatomic, assign) NSInteger synchronizedValue;
@end

@implementation SynchronizedClass
- (void)incrementSynchronizedValue {
    @synchronized(self) {
        self.synchronizedValue++;
    }
}
@end

在上述代码中,SynchronizedClass 类有一个 synchronizedValue 变量。incrementSynchronizedValue 方法使用 @synchronized(self) 块来同步对 synchronizedValue 的访问。@synchronized 会自动为传入的对象(这里是 self)创建一个锁,并在块开始时获取锁,在块结束时释放锁。

GCD 中的数据同步

Grand Central Dispatch(GCD)是苹果提供的一种高效的多线程编程模型。GCD 提供了一些机制来实现数据同步,与传统的锁机制不同,它更侧重于任务的调度和并发执行。

使用串行队列进行同步

在 GCD 中,我们可以使用串行队列来实现数据同步。因为串行队列中的任务是依次执行的,所以可以避免竞争条件。

@interface GCDSyncClass : NSObject
@property (nonatomic, assign) NSInteger gcdValue;
@end

@implementation GCDSyncClass
- (instancetype)init {
    self = [super init];
    if (self) {
        _gcdValue = 0;
    }
    return self;
}

- (void)incrementGCDValue {
    static dispatch_queue_t serialQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
    });

    dispatch_async(serialQueue, ^{
        self.gcdValue++;
    });
}
@end

在上述代码中,我们创建了一个静态的串行队列 serialQueue。每次调用 incrementGCDValue 方法时,都会将任务添加到串行队列中。由于串行队列的特性,任务会依次执行,从而保证了 gcdValue 的同步访问。

使用 dispatch_barrier_async 进行同步

dispatch_barrier_async 是 GCD 中一个非常有用的函数,它可以用于在并发队列中进行数据同步。当我们向并发队列中提交一个 barrier 任务时,在该任务之前提交到队列的所有任务都会先执行完毕,然后才执行 barrier 任务,并且在 barrier 任务执行期间,其他任务不会被执行,直到 barrier 任务完成。

@interface GCDBarrierClass : NSObject
@property (nonatomic, strong) NSMutableArray *sharedArray;
@end

@implementation GCDBarrierClass
- (instancetype)init {
    self = [super init];
    if (self) {
        _sharedArray = [NSMutableArray array];
    }
    return self;
}

- (void)addObjectToSharedArray:(id)object {
    static dispatch_queue_t concurrentQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    });

    dispatch_barrier_async(concurrentQueue, ^{
        [self.sharedArray addObject:object];
    });
}

- (id)objectAtIndex:(NSUInteger)index {
    __block id object;
    static dispatch_queue_t concurrentQueue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    });

    dispatch_sync(concurrentQueue, ^{
        object = self.sharedArray[index];
    });
    return object;
}
@end

在上述代码中,GCDBarrierClass 类有一个 NSMutableArray 实例 sharedArrayaddObjectToSharedArray: 方法使用 dispatch_barrier_async 向并发队列中添加一个 barrier 任务来添加对象到 sharedArrayobjectAtIndex: 方法使用 dispatch_sync 从并发队列中同步获取对象。这样可以保证在添加对象时,其他读取操作不会干扰,从而实现数据同步。

线程安全的数据结构

除了使用锁机制和 GCD 同步机制外,Objective - C 还提供了一些线程安全的数据结构,这些数据结构内部已经实现了同步机制,可以直接在多线程环境中使用。

NSCopying 和线程安全

对于一些遵循 NSCopying 协议的类,如 NSStringNSArrayNSDictionary 等,它们的不可变版本是线程安全的。这是因为不可变对象一旦创建,其内容就不能被修改,所以多个线程可以安全地访问。

NSString *immutableString = @"Hello, World!";
// 多个线程可以安全地访问 immutableString

而可变版本,如 NSMutableStringNSMutableArrayNSMutableDictionary 则不是线程安全的。如果需要在多线程环境中使用可变版本,可以通过锁机制或者 GCD 同步机制来保证数据安全。

线程安全的集合类

从 iOS 10 开始,苹果引入了一些线程安全的集合类,如 NSConcurrentDictionaryNSMutableOrderedSet

NSConcurrentDictionary *concurrentDict = [[NSConcurrentDictionary alloc] init];
// 多个线程可以安全地进行读写操作
[concurrentDict setObject:@"Value" forKey:@"Key"];
id value = [concurrentDict objectForKey:@"Key"];

NSConcurrentDictionary 内部实现了同步机制,允许多个线程同时进行读写操作,而不需要额外的锁机制。这在多线程环境中处理字典数据时非常方便。

选择合适的同步机制

在实际开发中,选择合适的同步机制非常重要。不同的同步机制有不同的优缺点,适用于不同的场景。

性能考量

锁机制,如 NSLockNSRecursiveLock 等,在加锁和解锁过程中会有一定的性能开销。特别是在高并发场景下,如果频繁加锁解锁,可能会影响程序的性能。@synchronized 关键字虽然使用方便,但性能也相对较低,因为它内部的实现比较复杂。

GCD 的同步机制,如使用串行队列和 dispatch_barrier_async,性能相对较高。因为 GCD 是基于队列的任务调度,在底层进行了优化,减少了锁的竞争。

线程安全的数据结构,如 NSConcurrentDictionary,由于内部已经实现了高效的同步机制,在多线程访问时性能也比较好。

场景适用性

如果是简单的同步需求,如对单个变量的读写保护,NSLock 或者 @synchronized 关键字可能就足够了。如果是递归函数中需要同步,NSRecursiveLock 是更好的选择。

对于需要根据条件进行同步的场景,NSConditionLock 比较合适。

在使用 GCD 时,如果需要对一组相关任务进行同步,使用串行队列比较方便。如果是在并发队列中需要对某些关键操作进行同步,dispatch_barrier_async 是很好的选择。

如果需要在多线程环境中频繁操作集合数据,使用线程安全的集合类,如 NSConcurrentDictionary,可以大大简化同步代码并提高性能。

死锁问题及避免

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

死锁示例

NSLock *lock1 = [[NSLock alloc] init];
NSLock *lock2 = [[NSLock alloc] init];

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, ^{
    [lock1 lock];
    [NSThread sleepForTimeInterval:1];
    [lock2 lock];
    // 执行一些操作
    [lock2 unlock];
    [lock1 unlock];
});

dispatch_async(queue2, ^{
    [lock2 lock];
    [NSThread sleepForTimeInterval:1];
    [lock1 lock];
    // 执行一些操作
    [lock1 unlock];
    [lock2 unlock];
});

在上述代码中,queue1 中的任务先获取 lock1,然后睡眠 1 秒,试图获取 lock2。而 queue2 中的任务先获取 lock2,然后睡眠 1 秒,试图获取 lock1。这样就会导致两个线程相互等待对方释放锁,从而发生死锁。

避免死锁的方法

  1. 按照固定顺序获取锁:在多个线程需要获取多个锁的情况下,确保所有线程都按照相同的顺序获取锁。例如,如果所有线程都先获取 lock1,再获取 lock2,就可以避免死锁。
  2. 使用超时机制:在获取锁时设置一个超时时间。如果在超时时间内没有获取到锁,线程可以放弃获取锁并进行其他操作,从而避免无限等待。例如,NSLock 类提供了 tryLock 方法,它会尝试获取锁,如果锁不可用,会立即返回 NO,而不会阻塞线程。
  3. 避免嵌套锁:尽量减少锁的嵌套使用。如果确实需要嵌套锁,要仔细设计锁的获取和释放顺序,确保不会出现循环等待的情况。

总结

在 Objective - C 多线程编程中,数据同步与锁机制是保证程序正确性和稳定性的关键。通过合理使用各种锁机制、GCD 同步机制以及线程安全的数据结构,我们可以有效地避免竞争条件和数据不一致问题。同时,要注意死锁问题的预防,确保程序在多线程环境下能够高效、稳定地运行。在实际开发中,需要根据具体的场景和性能需求,选择最合适的同步机制,以实现最优的多线程编程效果。

以上就是关于 Objective - C 多线程中的数据同步与锁机制的详细内容,希望对大家在多线程编程中有所帮助。在实际项目中,要不断实践和总结,才能更好地掌握这些技术,开发出高质量的多线程应用程序。