背景
目前iOS中保持线程同步的常用技术有:
- @synchronized
- NSLock
- NSRecursiveLock
- dispatch_semaphore
- NSConditionLock
- NSCondition
- pthread_mutex
- OSSpinLock
他们的实现机制各不相同,性能也各不一样,而我们平时用的最多的@synchronized是性能最差的,下面我们先分别介绍每个加锁方式的使用,再使用一个案例来对他们进行性能对比。
介绍与使用
@synchronized
NSObject *obj = [[NSObject alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj){
NSLog(@"需要线程同步的操作2");
}
});
2018-04-08 09:23:30.939801+0800 LockTest[5543:72013] 需要线程同步的操作1 开始
2018-04-08 09:23:33.944035+0800 LockTest[5543:72013] 需要线程同步的操作1 结束
2018-04-08 09:23:33.944273+0800 LockTest[5543:72014] 需要线程同步的操作2
@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(self),刚线程2就不会被阻塞
2018-04-08 09:25:42.358908+0800 LockTest[5654:74015] 需要线程同步的操作1 开始
2018-04-08 09:25:43.362748+0800 LockTest[5654:74012] 需要线程同步的操作2
2018-04-08 09:25:45.359381+0800 LockTest[5654:74015] 需要线程同步的操作1 结束
@synchronized指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制。
慎用@synchronized(self),最好不用@synchronized(self)
//class A
@synchronized (self) {
[_sharedLock lock];
NSLog(@"code in class A");
[_sharedLock unlock];
}
//class B
[_sharedLock lock];
@synchronized (objectA) {
NSLog(@"code in class B");
}
[_sharedLock unlock];
原因是因为self很可能会被外部对象访问,被用作key来生成一锁,类似上述代码中的@synchronized (objectA)。两个公共锁交替使用的场景就容易出现死锁。
所以正确的做法是传入一个类内部维护的NSObject对象,而且这个对象是对外不可见的
synchronized是使用的递归mutex来做同步。
@synchronized(nil)不起任何作用
NSLock
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//[lock lock];
[lock lockBeforeDate:[NSDate date]];
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
[lock unlock];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
if ([lock tryLock]) {
//尝试获取锁,如果获取不到返回NO,不会阻塞该线程
NSLog(@"锁可用的操作");
[lock unlock];
}else{
NSLog(@"锁不可用的操作");
}
NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:3];
if ([lock lockBeforeDate:date]) {
//尝试在未来的3s内获取锁,并阻塞该线程,如果3s内获取不到恢复线程, 返回NO,不会阻塞该线程
NSLog(@"没有超时,获得锁");
[lock unlock];
}else{
NSLog(@"超时,没有获得锁");
}
});
2018-04-08 09:32:04.450001+0800 LockTest[5948:78661] 需要线程同步的操作1 开始
2018-04-08 09:32:05.453375+0800 LockTest[5948:78662] 锁不可用的操作
2018-04-08 09:32:06.453399+0800 LockTest[5948:78661] 需要线程同步的操作1 结束
2018-04-08 09:32:06.453657+0800 LockTest[5948:78662] 没有超时,获得锁
NSLock是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,除lock和unlock方法外,NSLock还提供了tryLock和lockBeforeDate:两个方法,前一个方法会尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。lockBeforeDate:方法会在所指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
NSRecursiveLock
//NSLock *lock = [[NSLock alloc] init];
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1);
RecursiveMethod(value - 1);
}
[lock unlock];
};
RecursiveMethod(5);
});
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。如果使用NSLock,每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。
2018-04-08 09:44:56.819967+0800 LockTest[6521:88255] value = 5
2018-04-08 09:44:57.823937+0800 LockTest[6521:88255] value = 4
2018-04-08 09:44:58.825197+0800 LockTest[6521:88255] value = 3
2018-04-08 09:44:59.829992+0800 LockTest[6521:88255] value = 2
2018-04-08 09:45:00.831391+0800 LockTest[6521:88255] value = 1
dispatch_semaphore
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作2");
dispatch_semaphore_signal(signal);
});
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。
dispatch_semaphore_t dispatch_semaphore_create(long value);传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);这个函数会使传入的信号量dsema的值加1;
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);这个函数会使传入的信号量dsema的值减1;
这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。
2018-04-08 09:51:51.696376+0800 LockTest[6836:92885] 需要线程同步的操作1 开始
2018-04-08 09:51:53.700500+0800 LockTest[6836:92885] 需要线程同步的操作1 结束
2018-04-08 09:51:53.700734+0800 LockTest[6836:92884] 需要线程同步的操作2
如果overTime<2的话,在线程1还没有执行完成的情况下,此时超时了,将自动执行下面的代码
2018-04-08 10:01:13.383625+0800 LockTest[7261:99481] 需要线程同步的操作1 开始
2018-04-08 10:01:14.887388+0800 LockTest[7261:99483] 需要线程同步的操作2
2018-04-08 10:01:15.386684+0800 LockTest[7261:99481] 需要线程同步的操作1 结束
NSConditionLock
NSConditionLock *lock = [[NSConditionLock alloc] init];
NSMutableArray *products = [NSMutableArray array];
NSInteger HAS_DATA = 1;
NSInteger NO_DATA = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[lock lockWhenCondition:NO_DATA];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,总量:%zi",products.count);
[lock unlockWithCondition:HAS_DATA];
sleep(1);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
NSLog(@"wait for product");
[lock lockWhenCondition:HAS_DATA];
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[lock unlockWithCondition:NO_DATA];
}
});
当我们在使用多线程的时候,有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁:在线程1中的加锁使用了lock,所以是不需要条件的,所以顺利的就锁住了,但在unlock的使用了一个整型的条件,它可以开启其它线程中正在等待这把钥匙的临界地,而线程2则需要一把被标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开了线程2中的阻塞。但即便如此,NSConditionLock也跟其它的锁一样,是需要lock与unlock对应的,只是lock,lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的,当然这是与你的需求相关的。
2018-04-08 10:29:38.506916+0800 LockTest[8522:121834] produce a product,总量:1
2018-04-08 10:29:38.506951+0800 LockTest[8522:121836] wait for product
2018-04-08 10:29:38.507225+0800 LockTest[8522:121836] custome a product
2018-04-08 10:29:38.507317+0800 LockTest[8522:121836] wait for product
2018-04-08 10:29:39.511056+0800 LockTest[8522:121834] produce a product,总量:1
NSCondition
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *products = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
if ([products count] == 0) {
NSLog(@"wait for product");
[condition wait];
}
[products removeObjectAtIndex:0];
NSLog(@"custome a product");
[condition unlock];
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
while (1) {
[condition lock];
[products addObject:[[NSObject alloc] init]];
NSLog(@"produce a product,总量:%zi",products.count);
[condition signal];
[condition unlock];
sleep(1);
}
});
一种最基本的条件锁。手动控制线程wait和signal。[condition lock]一般用于多线程同时访问、修改同一个数据源,保证在同一时间内数据源只被访问、修改一次,其他线程的命令需要在lock外等待,直到unlock ,才可访问。[condition unlock]与lock 同时使用。[condition wait]让当前线程处于等待状态,[condition signal]CPU发信号告诉线程不用在等待,可以继续执行。
2018-04-08 11:04:57.238582+0800 LockTest[10134:149087] wait for product
2018-04-08 11:04:57.238953+0800 LockTest[10134:149089] produce a product,总量:1
2018-04-08 11:04:57.239133+0800 LockTest[10134:149087] custome a product
2018-04-08 11:04:57.239277+0800 LockTest[10134:149087] wait for product
2018-04-08 11:04:58.243059+0800 LockTest[10134:149089] produce a product,总量:1
pthread_mutex
__block pthread_mutex_t theLock;
pthread_mutex_init(&theLock, NULL);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
pthread_mutex_lock(&theLock);
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
pthread_mutex_unlock(&theLock);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
pthread_mutex_lock(&theLock);
NSLog(@"需要线程同步的操作2");
pthread_mutex_unlock(&theLock);
});
c语言定义下多线程加锁方式
- pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t *attr);初始化锁变量mutex
- attr为锁属性,NULL值为默认属性。pthread_mutex_lock(pthread_mutex_t *mutex);加锁
- pthread_mutex_tylock(pthread_mutex_t *mutex);加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待
- pthread_mutex_unlock(pthread_mutex_t *mutex);释放锁
- pthread_mutex_destroy(pthread_mutex_t *mutex);使用完后释放
2018-04-08 11:05:58.696100+0800 LockTest[10184:150540] 需要线程同步的操作1 开始
2018-04-08 11:06:01.699860+0800 LockTest[10184:150540] 需要线程同步的操作1 结束
2018-04-08 11:06:01.700143+0800 LockTest[10184:150541] 需要线程同步的操作2
pthread_mutex(recursive)
__block pthread_mutex_t theLock;
//pthread_mutex_init(&theLock, NULL);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&theLock, &attr);
pthread_mutexattr_destroy(&attr);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
static void (^RecursiveMethod)(int);
RecursiveMethod = ^(int value) {
pthread_mutex_lock(&theLock);
if (value > 0) {
NSLog(@"value = %d", value);
sleep(1); RecursiveMethod(value - 1);
}
pthread_mutex_unlock(&theLock);
};
RecursiveMethod(5);
});
这是pthread_mutex为了防止在递归的情况下出现死锁而出现的递归锁。作用和NSRecursiveLock递归锁类似。
如果使用pthread_mutex_init(&theLock, NULL);初始化锁的话,上面的代码会出现死锁现象。如果使用递归锁的形式,则没有问题
2018-04-08 11:14:36.143559+0800 LockTest[10553:157959] value = 5
2018-04-08 11:14:37.148126+0800 LockTest[10553:157959] value = 4
2018-04-08 11:14:38.151577+0800 LockTest[10553:157959] value = 3
2018-04-08 11:14:39.155052+0800 LockTest[10553:157959] value = 2
2018-04-08 11:14:40.159344+0800 LockTest[10553:157959] value = 1
OSSpinLock
在新版的iOS中已经不能再保证线程安全了,大神的链接。几个开源社区的应对:
苹果:查看 CoreFoundation 的源码能够发现,苹果至少在 2014 年就发现了这个问题,并把 CoreFoundation 中的 spinlock 替换成了 pthread_mutex,具体变化可以查看这两个文件:CFInternal.h(855.17)、CFInternal.h(1151.16)。苹果自己发现问题后,并没有更新 OSSpinLock 的文档,也没有告知开发者,这有些让人失望。
Google:google/protobuf 内部的 spinlock 被全部替换为 dispatch_semaphore,详情可以看这个提交:https://github.com/google/protobuf/pull/1060 用 dispatch_semaphore 而不用 pthread_mutex 应该是出于性能考虑。
性能测试
以iphone x模拟器为环境,模拟各种锁重复执行10000次所耗费的时间:
- (IBAction)testSynchronized:(id)sender {
NSObject *lock = [NSObject new];
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
@synchronized(lock) {}
}
NSTimeInterval end = CACurrentMediaTime();
printf("@synchronized: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testNSLock:(id)sender {
NSLock *lock = [NSLock new];
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
[lock lock];
[lock unlock];
}
NSTimeInterval end = CACurrentMediaTime();
printf("NSLock: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testNSRecursiveLock:(id)sender {
NSRecursiveLock *lock = [NSRecursiveLock new];
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
[lock lock];
[lock unlock];
}
NSTimeInterval end = CACurrentMediaTime();
printf("NSRecursiveLock: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testDispatchSemaphore:(id)sender {
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
dispatch_semaphore_signal(lock);
}
NSTimeInterval end = CACurrentMediaTime();
printf("dispatch_semaphore: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testNSConditionLock:(id)sender {
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1];
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
[lock lock];
[lock unlock];
}
NSTimeInterval end = CACurrentMediaTime();
printf("NSConditionLock: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testNSCondition:(id)sender {
NSCondition *lock = [NSCondition new];
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
[lock lock];
[lock unlock];
}
NSTimeInterval end = CACurrentMediaTime();
printf("NSCondition: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testPthreadMutex:(id)sender {
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
}
NSTimeInterval end = CACurrentMediaTime();
pthread_mutex_destroy(&lock);
printf("pthread_mutex: %8.2f ms\n", (end - begin) * 1000);
}
- (IBAction)testPthreadMutexRecursive:(id)sender{
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
NSTimeInterval begin = CACurrentMediaTime();
for (int i = 0; i < count; i++) {
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
}
NSTimeInterval end = CACurrentMediaTime();
pthread_mutex_destroy(&lock);
printf("pthread_mutex(recursive): %8.2f ms\n", (end - begin) * 1000);
}
将多次执行的结果绘制成图表可得
多线程同步性能测试.png
多线程到底哪里不安全?
共识:
结论一:同一块内存区域的访问是串行的,并不会导致内存数据的错乱或者应用的crash。
结论二:如果读写(load or store)的内存长度小于等于地址总线的长度,那么读写的操作是原子的,一次完成。比如bool,int,long在64位系统下的单次读写都是原子操作
值属性设置atomic
设置atomic之后,默认生成的getter和setter方法执行是原子的。也就是说,当我们在线程1执行getter方法的时候(创建调用栈,返回地址,出栈),线程B如果想执行setter方法,必须先等getter方法完成才能执行。
是不是使用了atomic就一定多线程安全呢?我们可以看看如下代码:
@interface ViewController ()
@property (atomic, assign) int intA;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread A: %d\n", self.intA);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < 10000; i ++) {
self.intA = self.intA + 1;
NSLog(@"Thread B: %d\n", self.intA);
}
});
}
即使我将intA声明为atomic,最后的结果也不一定会是20000。原因就是因为self.intA = self.intA + 1;不是原子操作,虽然intA的getter和setter是原子操作,但当我们使用intA的时候,整个语句并不是原子的,这行赋值的代码至少包含读取(load),+1(add),赋值(store)三步操作,当前线程store的时候可能其他线程已经执行了若干次store了,导致最后的值小于预期值。这种场景我们也可以称之为多线程不安全。
指针属性设置atomic
无论iOS系统是32位系统还是64位,一个指针的值都能通过一个指令完成load或者store。但和primitive type不同的是,对象类型还有内存管理的相关操作。在MRC时代,系统默认生成的setter类似如下:
- (void)setUserName:(NSString *)userName {
if(_uesrName != userName) {
[userName retain];
[_userName release];
_userName = userName;
}
}
不仅仅是赋值操作,还会有retain,release调用。如果property为nonatomic,上述的setter方法就不是原子操作,我们可以假设一种场景,线程1先通过getter获取当前_userName,之后线程2通过setter调用[_userName release];,线程1所持有的_userName就变成无效的地址空间了,如果再给这个地址空间发消息就会导致crash,出现多线程不安全的场景。
到了ARC时代,Xcode已经替我们处理了retain和release,绝大部分时候我们都不需要去关心内存的管理,但retain,release其实还是存在于最后运行的代码当中,atomic和nonatomic对于对象类的property声明理论上还是存在差异,而且通过实际测试也确实出现了这种情况,如在多线程情况下对nonatomic标识的NSString类型的属性进行读写,确实会偶发性报错
这一类多线程的访问场景是我们很容易出错的地方,即使我们声明property为atomic,依然会出错。因为我们访问的不是property的指针区域,而是property所指向的内存区域。可以看如下代码:
@interface ViewController ()
@property (atomic, strong) NSString *stringA;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
NSLog(@"Thread B: %@\n", self.stringA);
}
});
}
虽然stringA是atomic的property,而且在取substring的时候做了length判断,线程B还是很容易crash,因为在前一刻读length的时候self.stringA = @"a very long string";,下一刻取substring的时候线程A已经将self.stringA = @"string";,立即出现out of bounds的Exception,crash,多线程不安全。
同样的场景也适用于集合对象,如:
@interface ViewController ()
@property (atomic, strong) NSArray *arr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.arr = @[@"1", @"2", @"3"];
}else {
self.arr = @[@"1"];
}
NSLog(@"Thread A: %@\n", self.arr);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (self.arr.count >= 2) {
NSString* str = [self.arr objectAtIndex:1];
}
NSLog(@"Thread B: %@\n", self.arr);
}
});
}
同理,即使我们在访问objectAtIndex之前做了count的判断,线程B依旧很容易crash,原因也是由于前后两行代码之间arr所指向的内存区域被其他线程修改了。
所以,真正需要操心的是这一类内存区域的访问,即使声明为atomic也没有用,我们平常App出现莫名其妙难以重现的多线程crash多是属于这一类,一旦在多线程的场景下访问这类内存区域的时候,要提起十二分的小心。
property多线程总结
简而言之,atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。
[_lock lock];
if (self.arr.count >= 2) {
NSString* str = [self.arr objectAtIndex:1];
}
[_lock unlock];
网友评论