引言
在多线程编程中,数据一致性是一个必须解决的问题。多个线程同时访问同一片共享数据时,极易发生竞争条件(race conditions),导致数据的不一致性,甚至程序崩溃。为了解决这些问题,我们需要引入一种机制,确保多个线程之间的操作可以安全地进行。
在Objective-C中,atomic是一种常见建的锁机制,能欧有效地防止资源竞争,保障数据的安全性。atomic常用于确保属性读写的原子性,避免数据在多线程环境下被破坏;
在本篇博客中,我们将深入探讨这种锁机制的工作原理、使用场景以及它的优缺点,并通过Objective-C的示例代码展示如何在实际项目中应用它来确保线程安全。
Atomic(属性修饰符)
概述
atomic是Objective-C中的属性修饰符,它可以确保属性读写时线程安全的。
原子属性是实现应用状态线程安全的一个良好开始,如果一个属性是使用atomic修饰的,则它的修改和读取肯定都是原子的。这一点很重要,因为这个可以阻止两个线程同时更新一个值,反之则有可能导致错误的状态。正在修改属性的线程必须处理完毕之后,其它线程才能开始处理。
在Objective-C中,属性默认是atomic的,这意味着编译器会自动为属性的getter和setter方法加锁,确保属性的读写操作时线程安全的。然而,如果我们显示地将属性标记为nonatomic,编译器就不会为我们提供这种保护。虽然这可以提升一些性能,但也意味着在并发环境下,多线程可能同时读取和写入该属性,从而导致数据竞争(race condition)。
nonatomic 多线程操作
在这个示例中,我们使用一个nonatomic修饰的属性address,并通过GCD创建了多个并发线程来设置和打印这个属性的值:
//MARK: nonatomic修饰的属性
- (void)nonatomicTest {
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.address = [NSString stringWithFormat:@"河北省唐山市路北区%d", i];
NSLog(@"%@", self.address);
});
}
}
因为addres是nonatomic的,所以在多线程同时写入时,并不能保证数据的完整性和一致性。可能会发生一些异常情况,比如输出的地址内容会出现错乱,或者值还没有完全写入就被领一个线程读取,从而导致不完整的输出,或者崩溃。通常会崩溃在objec_release或者objct_retian相关的操作中。
atomic 多线程操作
而如果我们使用atomic来修饰address属性,则可以保证它的读写操作是线程安全的。
//MARK: atomic修饰的属性
- (void)atomicTest {
for (int i = 0; i < 1000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.address = [NSString stringWithFormat:@"河北省唐山市路北区%d", i];
NSLog(@"%@", self.address);
});
}
}
由于atomic修饰符的影响,它保证了对该属性的每次读写操作都是线程安全的。也就是说,在多线程环境中,访问address属性时,每次的读写操作都不会被其他线程打断。
编译器会自动为address的getter和setter方法加锁,确保在并发操作时数据的完整性和一致性。
但是atomic虽然确保了线程安全,但它引入了额外的开销,每次对address的读写都需要加锁和解锁,这可能会对性能产生一定影响,尤其是在高并发的情况下。
atomic局限
atomic虽然可以保证单个数据的读写是线程安全的,但是却不能保证复合数据的一致性,原子属性通常作用于单个变量或数据操作,对于多个数据,或者数组并不起作用,因为数组通常包含多个元素,原子操作无法直接应用整个数组。
假设我们现在有一个HPUser实体类,通过服务类可以对其数据进行更新。
//一个实体(部分定义)
@interface HPUser
@property(atomic,copy)NSString * firstName;
@property(atomic,copy)NSString * lastName;
@end
//一个服务类(出于简洁的目的省略了声明)
@implementation HPUpdaterService
- (void)updateUser:(HPUser *)user properties:(NSDictionary *)properties{
NSString * fn = [properties objectForKey:@“firstName”];
if(fn != nil){
user.firstName = fn;
}
NSString * ln = [properties objectForKey:@“lastName”];
if(fn != nil){
user.lastName = ln;
}
}
@end
每当用户刷线数据,数据从服务器返回时都会调用updateUser:properties:方法。此外,一个周期性执行的同步任务也会调用该方法。
因此,在某个时间点可能会有多个响应同时尝试更新用户配置文件。
当有两个响应在不同的线程中试图更新用户,名称分别为Bob Taylor和Alice Darji。如果不对属性firstName和lastName使用原子更新,则根本无法确保执行顺序,最终的结果可能是任意组合,其中包括Alice Taylor和Bob Darji。
这时候我们就看见了atomic原子属性的局限性,但我们仍然有其它方案可以解决这个问题,我们会在后面的博客中继续讨论它。
结语
在这篇博客中,我们深入探讨了并发编程中的锁机制,主要是Objective-C中的atomic属性。通过对atomic属性的详细分析,我们了解了它如何确保在多线程环境中的线程安全性。以及它在保证数据一致性方面的作用和性能开销。
然而,atomic并不是解决所有线程安全问题的终极方案。在一些复杂的并发场景中,可能需要更灵活的同步机制来确保多个线程间的协调和数据一致性。接下来,我们将探索另一种强大的同步工具——@synchronized。