OC之锁
前言:多线程下的data race
问题
demo
我们在利用并发编程以更高效率处理任务的同时,也带来了一些问题:如对共享内存的读写操作。因为要同时执行多个线程,所以整个程序的执行顺序是不同的,使用不同的执行顺序执行同一段代码得到不同的结果,这会导致并发程序中的bug难以检测和修复。
我们不妨来看一个多个线程同时修改一个数据的线程安全的例子:
- (void)testDataSafetyMethod
{
dispatch_queue_t queue1 = dispatch_queue_create("com.demo.task1", DISPATCH_QUEUE_CONCURRENT);
__block int length = 0;
dispatch_apply(6, queue1, ^(size_t index) {
dispatch_async(queue1, ^{
NSLog(@"length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"length --- %d == %@",--length,NSThread.currentThread);
});
});
}
看下它的打印结果
18:57:45.073842 length === 3 -- <NSThread: 0x60400046b2c0>{number = 6, name = (null)}
18:57:45.073842 length === 1 -- <NSThread: 0x600000467100>{number = 3, name = (null)}
18:57:45.073848 length === 4 -- <NSThread: 0x600000467780>{number = 5, name = (null)}
18:57:45.073887 length === 2 -- <NSThread: 0x60400046b0c0>{number = 4, name = (null)}
18:57:45.074246 length === 5 -- <NSThread: 0x60400046bb40>{number = 7, name = (null)}
18:57:45.074355 length === 6 -- <NSThread: 0x60400046b4c0>{number = 8, name = (null)}
18:57:47.075042 length --- 5 == <NSThread: 0x600000467100>{number = 3, name = (null)}
18:57:47.075042 length --- 5 == <NSThread: 0x60400046b2c0>{number = 6, name = (null)}
18:57:47.075042 length --- 4 == <NSThread: 0x600000467780>{number = 5, name = (null)}
18:57:47.075648 length --- 3 == <NSThread: 0x60400046b0c0>{number = 4, name = (null)}
18:57:47.075649 length --- 2 == <NSThread: 0x60400046bb40>{number = 7, name = (null)}
18:57:47.075707 length --- 1 == <NSThread: 0x60400046b4c0>{number = 8, name = (null)}
两个线程同时访问同一个变量,而且其中至少一个线程要做的是写操作,这种情况就叫竞态。Xcode 有一个线程竞态检测工具 Thread Sanitizer
可以检测出这类问题。当我们开启 Thread Sanitizer
时,控制台还输出了以下警告:
==================
WARNING: ThreadSanitizer: data race (pid=1083)
Write of size 4 at 0x7b080005ce58 by thread T10:
#0 __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke_2 ThreadSafetyTableController.m:119 (ThreadDemo:x86_64+0x1000179d2)
#1 __tsan::invoke_and_release_block(void*) <null>:3746304 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x622bb)
#2 _dispatch_client_callout <null>:3746304 (libdispatch.dylib:x86_64+0x37eb)
Previous write of size 4 at 0x7b080005ce58 by thread T8:
#0 __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke_2 ThreadSafetyTableController.m:119 (ThreadDemo:x86_64+0x1000179d2)
#1 __tsan::invoke_and_release_block(void*) <null>:3746304 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x622bb)
#2 _dispatch_client_callout <null>:3746304 (libdispatch.dylib:x86_64+0x37eb)
Location is heap block of size 32 at 0x7b080005ce40 allocated by main thread:
#0 malloc <null>:3746320 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x46fea)
#1 _Block_object_assign <null>:3746320 (libsystem_blocks.dylib:x86_64+0xbbb)
#2 _Block_copy <null>:3746320 (libsystem_blocks.dylib:x86_64+0x8cd)
#3 __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke ThreadSafetyTableController.m:118 (ThreadDemo:x86_64+0x100017946)
#4 __wrap_dispatch_apply_block_invoke <null>:3746320 (libclang_rt.tsan_iossim_dynamic.dylib:x86_64+0x64491)
#5 _dispatch_client_callout2 <null>:3746320 (libdispatch.dylib:x86_64+0x381c)
#6 -[ThreadSafetyTableController testDataSafetyMethod] ThreadSafetyTableController.m:116 (ThreadDemo:x86_64+0x100017856)
#7 -[ThreadSafetyTableController tableView:didSelectRowAtIndexPath:] ThreadSafetyTableController.m:65 (ThreadDemo:x86_64+0x1000170f9)
#8 -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] <null>:3746320 (UIKit:x86_64+0x168e88)
#9 start <null>:3746320 (libdyld.dylib:x86_64+0x1954)
Thread T10 (tid=36092, running) is a GCD worker thread
Thread T8 (tid=36081, running) is a GCD worker thread
SUMMARY: ThreadSanitizer: data race ThreadSafetyTableController.m:119 in __51-[ThreadSafetyTableController testDataSafetyMethod]_block_invoke_2
==================
ThreadSanitizer report breakpoint hit. Use 'thread info -s' to get extended information about the report.
我们可以看到,程序在运行期间,报出:data race
的警告。
共享内存模式需要一种机制来协调多个线程共用的数据。通常使用同步机制来实现这一目标,如锁或者判定条件:
- 锁:是一种控制多线程间数据同步访问和资源共享的一种机制。
- 条件:条件变量时一种同步机制,它使线程一直处于等待状态直到指定条件出现,条件变量通常用锁来实现。
锁是最常见的控制机制,使用它可以控制多线程对共享数据的访问。锁实施是一种互斥策略,从而避免受保护的数据和资源被多个线程同时访问。但是:使用锁协调对共享数据的访问时,很有可能引发死锁、活锁、资源匮乏等问题,这些问题导致程序中断;
- 死锁:指两个或多个线程互相阻塞的情况,每个线程都等待其他线程释放锁,导致所有线程都处于等待状态。典型的例子就是循环等待
- 活锁:指一个线程因为要回应其它的一个或多个线程,而导致自身无法执行的情况,活锁的线程没有被阻塞,它将所有的计算时间用于回应其它线程,以恢复正常的操作。
- 资源匮乏:指线程无法正常访问共享资源的情况,通常是共享资源被其它线程占用。当一个或多个线程占用共享资源的时间过长,就会引发这种问题。活锁也是资源匮乏的一种形式。
苹果为我们提供了多种安全机制来让我们对共享内存进行访问:
1、互斥锁
互斥锁是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。在Objective-C
种有三种互斥锁,我们来看下
1.1、@synchronized ()
锁
@synchronized这个结构发挥了和锁一样的作用:它避免了多个线程同时执行同一段代码。和使用NSLock进行创建锁、加锁、解锁相比,在某些情况下@synchronized会更方便、更易读。
- (void)synchronizedMethod
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskA --- 执行前");
NSLog(@"taskA === %@",[self getTheString]);
NSLog(@"taskA --- 执行后");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskB --- 执行前");
NSLog(@"taskB === %@",[self getTheString]);
NSLog(@"taskB --- 执行后");
});
}
- (NSString *)getTheString
{
@synchronized (self)
{
NSLog(@"currentThread == %@",NSThread.currentThread);
[NSThread sleepForTimeInterval:2];
return @"*****************************";
}
}
运行程序,查看控制台输出:
19:18:03.347253 taskA --- 执行前
19:18:03.347261 taskB --- 执行前
19:18:03.347567 currentThread == <NSThread: 0x60400046e600>{number = 3, name = (null)}
19:18:05.349432 taskB === *****************************
19:18:05.349570 currentThread == <NSThread: 0x604000471e80>{number = 4, name = (null)}
19:18:05.349728 taskB --- 执行后
19:18:07.351586 taskA === *****************************
19:18:07.351929 taskA --- 执行后
1.2、NSLock
锁
NSLock 是Cocoa提供给我们最基本的锁对象,这也是我们经常所使用的,使用POSIX线程实现其锁定行为。我们先来看下NSLock
的API:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
//NSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK。
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock; //tryLock 并不会阻塞线程。[lock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码
- (BOOL)lockBeforeDate:(NSDate *)limit;//lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
NSLock
有三种方法来执行加锁操作,那么这三个方法有何区别呢?我们使用一段代码,根据不同时间下的加锁操作,分别测试下 NSLock
的三种加锁方法的特性:
- (void)testLockMethod
{
NSLog(@"开始处理 ---- %@",NSThread.currentThread);
__block int length = 0;
NSLock *lock = [[NSLock alloc] init];//初始化一个 NSLock 实例
//将 taskA 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskA === 开始尝试加锁");
[lock lock];//给 taskA 加锁
NSLog(@"taskA === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskA === length --- %d == %@",--length,NSThread.currentThread);
[lock unlock];//taskA 完成后解锁
NSLog(@"taskA --- 已经解锁");
});
//将 taskB 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskB === 开始尝试加锁");
[lock lock];//给 taskB 加锁
NSLog(@"taskB === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];
NSLog(@"taskB === length --- %d == %@",--length,NSThread.currentThread);
[lock unlock];//taskB 完成后解锁
NSLog(@"taskB --- 已经解锁");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//tryLock 并不会阻塞线程。[lock tryLock] 能加锁返回 YES,不能加锁返回 NO,然后都会执行后续代码。
NSLog(@"taskC === 开始尝试加锁");
if ([lock tryLock])
{
NSLog(@"taskC === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskC === length --- %d == %@",--length,NSThread.currentThread);
[lock unlock];//taskC 完成后解锁
NSLog(@"taskC --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskC === length --- %d == %@",length,NSThread.currentThread);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO,指定时间之前能加锁,则返回 YES。
//taskD 尝试在 3s 之后加锁,
NSLog(@"taskD === 开始尝试加锁");
if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:3]]){
NSLog(@"taskD === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];
NSLog(@"taskD === length --- %d == %@",--length,NSThread.currentThread);
[lock unlock];
NSLog(@"taskD --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskD === length --- %d == %@",length,NSThread.currentThread);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//taskE 尝试在 10s 之后加锁 ,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
NSLog(@"taskE === 开始尝试加锁");
if ([lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]){
NSLog(@"taskE === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];
NSLog(@"taskE === length --- %d == %@",--length,NSThread.currentThread);
[lock unlock];
NSLog(@"taskE --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskE === length --- %d == %@",length,NSThread.currentThread);
}
});
//延迟 15s 执行 ,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskF === 开始尝试加锁");
if ([lock tryLock])
{
NSLog(@"taskF === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];
NSLog(@"taskF === length --- %d == %@",--length,NSThread.currentThread);
[lock unlock];
NSLog(@"taskF --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskF === length --- %d == %@",length,NSThread.currentThread);
}
});
NSLog(@"结束处理 ---- %@",NSThread.currentThread);
}
这段代码里有 taskA
、taskB
... taskF
总计六个任务,我们尝试以三种方法分别为这六个任务加锁操作;
-
taskA
、taskB
使用- (void)lock;
方法加锁操作 -
taskC
、taskF
使用- (BOOL)tryLock;
方法加锁操作 -
taskD
、taskE
使用- (BOOL)lockBeforeDate:(NSDate *)limit;
方法加锁操作
我们运行这段代码,观察输出结果:
09:40:17 taskA === 开始尝试加锁
09:40:17 taskA === length === 1 -- <NSThread: 0x600000466ec0>{number = 3, name = (null)}
09:40:17 taskB === 开始尝试加锁
09:40:17 taskC === 开始尝试加锁
09:40:17 taskD === 开始尝试加锁
09:40:17 taskE === 开始尝试加锁
09:40:17 加锁失败 :taskC === length --- 1 == <NSThread: 0x60400027fec0>{number = 4, name = (null)}
09:40:19 taskA === length --- 0 == <NSThread: 0x600000466ec0>{number = 3, name = (null)}
09:40:19 taskA --- 已经解锁
09:40:19 taskB === length === 1 -- <NSThread: 0x604000460a40>{number = 5, name = (null)}
09:40:20 加锁失败 :taskD === length --- 1 == <NSThread: 0x6040004607c0>{number = 6, name = (null)}
09:40:21 taskB === length --- 0 == <NSThread: 0x604000460a40>{number = 5, name = (null)}
09:40:21 taskB --- 已经解锁
09:40:21 taskE === length === 1 -- <NSThread: 0x60000027db00>{number = 7, name = (null)}
09:40:23 taskE === length --- 0 == <NSThread: 0x60000027db00>{number = 7, name = (null)}
09:40:23 taskE --- 已经解锁
09:40:32 taskF === 开始尝试加锁
09:40:32 taskF === length === 1 -- <NSThread: 0x600000068cc0>{number = 7, name = (null)}
09:40:34 taskF === length --- 0 == <NSThread: 0x600000068cc0>{number = 7, name = (null)}
09:40:34 taskF --- 已经解锁
从控制台输出可以看到:数据 length
在多线程中操作被加锁之后是线程安全的,这方便不过多讨论,我们主要讨论下 NSLock
的三个方法执行特点:
- 从
taskA
与taskB
打印时间来看:- (void)lock;
会阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码; - 从
taskC
的打印时间可以看到:- (BOOL)tryLock;
不会阻塞当前线程的执行,不能加锁直接返回NO
,然后去执行后续代码;
taskF
在很久以后才开始加锁,这时加锁成功:- (BOOL)tryLock;
能加锁直接返回YES
,然后去执行后续代码; - 从
taskD
的打印结果可以看到,从开始执行加锁代码到加锁失败经历了3s
,也就是说:- lockBeforeDate:
方法会阻塞当前线程的执行,直到指定的时间为止,如果还不能加锁,返回NO
,然后去执行后续代码;
从taskE
的打印结果可以看到,从开始执行加锁代码到加锁失败经历了4s
,这时- lockBeforeDate:
方法成功加锁返回YES
,然后去执行后续代码;
总结 NSLock
的三个加锁方法:
加锁方法 | 描述 |
---|---|
- (void)lock; |
会阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码; |
- (BOOL)tryLock; |
不会阻塞当前线程的执行,不能加锁直接返回 NO /能加锁直接返回 YES ,然后去执行后续代码; |
- (BOOL)lockBeforeDate:(NSDate *)limit; |
在所指定 Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回NO ,指定时间之前能加锁,则返回YES 。 |
1.3、pthread_mutex_t
pthread_mutex_t
是C语言定义下多线程加锁方式,它是Unix/Linux
平台上提供的一套条件互斥锁的API
。使用pthread_mutex_t
,需要引入头文件#include <pthread.h>
。pthread_mutex_t
除了创建互斥锁,还可以创建递归锁、读写锁、条件锁等。
我们利用pthread_mutex_t
的API
提供的函数,来实现一个互斥锁:
- (void)pthread_mutex_LockMethod
{
NSLog(@"开始处理 ---- %@",NSThread.currentThread);
dispatch_group_t group = dispatch_group_create();
__block pthread_mutex_t pthreadLock = PTHREAD_MUTEX_INITIALIZER;//静态创建一个互斥锁
__block int length = 0;
//将 taskA 放到一个并发队列中异步执行
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskA === 开始尝试加锁");
pthread_mutex_lock(&pthreadLock);//给 taskA 加锁
NSLog(@"taskA === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskA === length --- %d == %@",--length,NSThread.currentThread);
pthread_mutex_unlock(&pthreadLock);//taskA 完成后解锁
NSLog(@"taskA --- 已经解锁");
});
//将 taskB 放到一个并发队列中异步执行
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskB === 开始尝试加锁");
pthread_mutex_lock(&pthreadLock);//给 taskB 加锁
NSLog(@"taskB === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];
NSLog(@"taskB === length --- %d == %@",--length,NSThread.currentThread);
pthread_mutex_unlock(&pthreadLock);//taskB 完成后解锁
NSLog(@"taskB --- 已经解锁");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskC === 开始尝试加锁");
if (pthread_mutex_trylock(&pthreadLock) == 0)
{
NSLog(@"taskC === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskC === length --- %d == %@",--length,NSThread.currentThread);
pthread_mutex_unlock(&pthreadLock);//taskC 完成后解锁
NSLog(@"taskC --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskC === length --- %d == %@",length,NSThread.currentThread);
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:6];//堵塞当前线程6s,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
NSLog(@"taskD === 开始尝试加锁");
if (pthread_mutex_trylock(&pthreadLock) == 0)
{
NSLog(@"taskD === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskD === length --- %d == %@",--length,NSThread.currentThread);
pthread_mutex_unlock(&pthreadLock);//taskC 完成后解锁
NSLog(@"taskD --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskD === length --- %d == %@",length,NSThread.currentThread);
}
});
//延迟 10 执行 ,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskE === 开始尝试加锁");
if (pthread_mutex_trylock(&pthreadLock) == 0)
{
NSLog(@"taskE === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskE === length --- %d == %@",--length,NSThread.currentThread);
pthread_mutex_unlock(&pthreadLock);//taskC 完成后解锁
NSLog(@"taskE --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskE === length --- %d == %@",length,NSThread.currentThread);
}
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"group中所有任务完成 ---- %@",NSThread.currentThread);
pthread_mutex_destroy(&pthreadLock);
});
}
因为 pthread_mutex
是使用 C 语言创建的,编译器并不负责 pthread_mutex
的释放,所以我们使用任务组dispatch_group
,希望在任务组接收到通知后手动释放pthread_mutex
。
我们创建了taskA
、taskB
...taskE
五个任务,这五个任务开始执行上锁的时间不一样,使用上锁的方式有两种:pthread_mutex_lock()
与 pthread_mutex_trylock()
;
我们通过运行程序观察控制台打印,来探讨下这两种方式下加锁操作的不同。
13:58:37 开始处理 ---- <NSThread: 0x604000067c80>{number = 1, name = main}
13:58:37 taskA === 开始尝试加锁
13:58:37 taskB === 开始尝试加锁
13:58:37 taskC === 开始尝试加锁
13:58:37 taskB === length === 1 -- <NSThread: 0x60000046de40>{number = 3, name = (null)}
13:58:37 加锁失败 :taskC === length --- 1 == <NSThread: 0x60000046a240>{number = 4, name = (null)}
13:58:39 taskB === length --- 0 == <NSThread: 0x60000046de40>{number = 3, name = (null)}
13:58:39 taskB --- 已经解锁
13:58:39 taskA === length === 1 -- <NSThread: 0x60000046a200>{number = 5, name = (null)}
13:58:41 taskA === length --- 0 == <NSThread: 0x60000046a200>{number = 5, name = (null)}
13:58:41 taskA --- 已经解锁
13:58:43 taskD === 开始尝试加锁
13:58:43 taskD === length === 1 -- <NSThread: 0x600000470fc0>{number = 6, name = (null)}
13:58:45 taskD === length --- 0 == <NSThread: 0x600000470fc0>{number = 6, name = (null)}
13:58:45 taskD --- 已经解锁
13:58:45 group中所有任务完成 ---- <NSThread: 0x604000067c80>{number = 1, name = main}
13:58:47 taskE === 开始尝试加锁
13:58:47 加锁失败 :taskE === length --- 0 == <NSThread: 0x600000470fc0>{number = 6, name = (null)}
- 通过
taskA
与taskB
可以看到:pthread_mutex_t
实现了NSLocking
协议相同效果的互斥功能:在任一时刻,只能有一个线程操作数据length
。
其中:int pthread_mutex_lock(pthread_mutex_t *)
类似于- (void)lock
执行上锁操作;int pthread_mutex_unlock(pthread_mutex_t *)
类似于- (void)unlock
执行解锁操作 - 再来看
taskC
与taskD
:taskC
与taskA
、taskB
同时执行加锁操作,但是taskC
使用pthread_mutex_trylock()
加锁,返回一个非0
值,也就是加锁失败;而taskD
所在线程被堵塞了6s
,在开始处理6s
后才开始加锁,这保证了pthread_mutex
当前没有上锁,打印结果表明taskD
上锁成功,返回0
;这表明了pthread_mutex_trylock()
类似于NSLock
的- (BOOL)tryLock
方法,不管加锁成功或者失败,都不会堵塞当前线程,会立即返回加锁结果; -
taskD
解锁之后,群组里面所有任务执行完毕,收到通知调用pthread_mutex_destroy()
释放pthread_mutex
(此处说明下:dispatch_after()
函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到队列中,此时还不到10s
,所以任务组dispatch_group
中没有任何任务) - 我们可以看到
taskE
理所当然的加锁失败,这是因为taskE
上锁时该锁已经被释放掉
执行 | 方法 | 方法描述 |
---|---|---|
静态创建 | 宏 PTHREAD_MUTEX_INITIALIZER
|
静态创建一个互斥锁 |
加锁 | int pthread_mutex_lock(pthread_mutex_t *) |
会阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码 |
加锁 | int pthread_mutex_trylock(pthread_mutex_t *) |
不会阻塞当前线程的执行,不能加锁直接返回 非0 值 /能加锁直接返回 0 ,然后去执行后续代码 |
解锁 | int pthread_mutex_unlock(pthread_mutex_t *) |
解锁 |
销毁 | int pthread_mutex_destroy(pthread_mutex_t *) |
释放指定锁 |
使用互斥量需要注意的几点:
- 互斥量需要时间来加锁和解锁。锁住较少互斥量的程序通常运行得更快。所以,互斥量应该尽量少,够用即可,每个互斥量保护的区域应则尽量大。
- 互斥量的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥量,则线程的大部分时间就会在等待,这对性能是有害的。如果互斥量保护的数据(或代码)包含彼此无关的片段,则可以特大的互斥量分解为几个小的互斥量来提高性能。这样,任意时刻需要小互斥量的线程减少,线程等待时间就会减少。所以,互斥量应该足够多(到有意义的地步),每个互斥量保护的区域则应尽量的少。
2、 递归锁
同一个线程可以多次加锁,不会造成死锁
2.1、 递归死锁
You should not use this class to implement a recursive lock. Calling the lock method twice on the same thread will lock up your thread permanently. Use the NSRecursiveLock class to implement recursive locks instead.
官方文档警告我们:在递归中我们不能使用NSLock
,建议我们使用NSRecursiveLock
。我们不妨先看下使用 NSLock
在递归中运行的效果:
- (void)recursiveLockMethod
{
NSLog(@"开始处理 ------------ %@",NSThread.currentThread);
NSLock *lock = [[NSLock alloc] init];
static void (^recursiveTestBlock)(int length);
//使用 NSThread 开辟一条新线程执行递归操作处理taskA
[NSThread detachNewThreadWithBlock:^{
recursiveTestBlock = ^(int length)
{
NSLog(@"开始加锁 : taskA === length === %d -- %@",length,NSThread.currentThread);
[lock lock];
if (length > 0)
{
[NSThread sleepForTimeInterval:2];//模拟耗时任务
recursiveTestBlock(--length);
}
[lock unlock];
NSLog(@"已经解锁 : taskA --- length --- %d == %@",length,NSThread.currentThread);
};
recursiveTestBlock(3);
}];
//使用 NSThread 开辟一条新线程执行递归操作处理taskB
[NSThread detachNewThreadWithBlock:^{
NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
[lock lock];
[NSThread sleepForTimeInterval:2];//模拟耗时任务
[lock unlock];
NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
}];
NSLog(@"结束处理 ------------ %@",NSThread.currentThread);
}
下面是输出语句:
14:07:53 开始加锁 : taskA === length === 3 -- <NSThread: 0x60400046fe40>{number = 5, name = (null)}
14:07:53 开始加锁 : taskB === <NSThread: 0x604000474a40>{number = 6, name = (null)}
14:07:55 开始加锁 : taskA === length === 2 -- <NSThread: 0x60400046fe40>{number = 5, name = (null)}
可以很明显的看到,线程被锁死了,代码无法接着执行;我们来分析下原因:
首先分别在两个线程里分别执行了加锁操作:从前面对 NSLock
的分析来看,taskA
加锁成功,taskB
会堵塞线程 6 ,等待线程 5 解锁后才接着执行;
然后线程 5 里,taskA
做递归,再次执行加锁操作,这时加锁不会成功,会堵塞 线程 5 直到解锁,但是代码不往下执行,就不会解锁;这样子就造成了死锁
2.2、NSRecursiveLock
递归锁
我们按照苹果官方的建议使用 NSRecursiveLock
来处理上述代码:
- (void)recursiveLockMethod1
{
NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
static void (^recursiveTestBlock)(int length);
//使用 NSThread 开辟一条新线程执行递归操作处理taskA
[NSThread detachNewThreadWithBlock:^{
recursiveTestBlock = ^(int length)
{
NSLog(@"开始加锁 : taskA === length === %d -- %@",length,NSThread.currentThread);
[recursiveLock lock];
if (length > 0)
{
[NSThread sleepForTimeInterval:2];
recursiveTestBlock(--length);
}
[recursiveLock unlock];
NSLog(@"已经解锁 : taskA --- length --- %d == %@",length,NSThread.currentThread);
};
recursiveTestBlock(3);
}];
//使用 NSThread 开辟一条新线程处理taskB
[NSThread detachNewThreadWithBlock:^{
NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
[recursiveLock lock];
[NSThread sleepForTimeInterval:2];//模拟耗时任务
[recursiveLock unlock];
NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
}];
}
以下是输出语句:
14:05:20 开始加锁 : taskA === length === 3 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:20 开始加锁 : taskB === <NSThread: 0x60000047aec0>{number = 4, name = (null)}
14:05:22 开始加锁 : taskA === length === 2 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:24 开始加锁 : taskA === length === 1 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:26 开始加锁 : taskA === length === 0 -- <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:26 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:26 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:26 已经解锁 : taskA --- length --- 1 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:26 已经解锁 : taskA --- length --- 2 == <NSThread: 0x60400046ff00>{number = 3, name = (null)}
14:05:28 已经解锁 : taskB === <NSThread: 0x60000047aec0>{number = 4, name = (null)}
- 从
taskA
的打印可以看出:NSRecursiveLock
可以在一个线程中重复加锁(反正单线程内任务是按顺序执行的,不会出现资源竞争问题), - 从
taskB
的打印可以看出:NSRecursiveLock
会记录上锁和解锁的次数,当二者平衡的时候,才会释放锁,其它线程才可以上锁成功。
2.3、利用pthread_mutex_t
实现递归锁
在1.3中我们简单的了解了pthread_mutex_t
互斥锁,知道了它的简单上锁/解锁的用法,现在我们再来探讨下如何利用pthread_mutex_t
实现递归锁的效果:
- (void)pthread_mutex_RecursiveLockMethod
{
__block pthread_mutex_t pthreadLock;
pthread_mutexattr_t pthreadMutexattr;
pthread_mutexattr_init(&pthreadMutexattr);
pthread_mutexattr_settype(&pthreadMutexattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&pthreadLock, &pthreadMutexattr);//初始化
static void (^recursiveTestBlock)(int length);
//使用 NSThread 开辟一条新线程执行递归操作处理taskA
[NSThread detachNewThreadWithBlock:^{
recursiveTestBlock = ^(int length)
{
NSLog(@"开始加锁 : taskA === length === %d -- %@",length,NSThread.currentThread);
pthread_mutex_lock(&pthreadLock);//上锁
if (length > 0)
{
[NSThread sleepForTimeInterval:2];
recursiveTestBlock(--length);
}
pthread_mutex_unlock(&pthreadLock);//解锁
NSLog(@"已经解锁 : taskA --- length --- %d == %@",length,NSThread.currentThread);
};
recursiveTestBlock(3);
}];
//使用 NSThread 开辟一条新线程处理taskB
[NSThread detachNewThreadWithBlock:^{
NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
pthread_mutex_lock(&pthreadLock);//上锁
[NSThread sleepForTimeInterval:2];//模拟耗时任务
pthread_mutex_unlock(&pthreadLock);//解锁
NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
}];
}
代码和 2.2 的代码相同,只是将实现递归的锁换为pthread_mutex_t
,我们来看下输出结果:
14:56:55 开始加锁 : taskB === <NSThread: 0x604000464580>{number = 4, name = (null)}
14:56:55 开始加锁 : taskA === length === 3 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:56:57 开始加锁 : taskA === length === 2 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:56:59 开始加锁 : taskA === length === 1 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:57:01 开始加锁 : taskA === length === 0 -- <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:57:01 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:57:01 已经解锁 : taskA --- length --- 0 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:57:01 已经解锁 : taskA --- length --- 1 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:57:01 已经解锁 : taskA --- length --- 2 == <NSThread: 0x60400027f780>{number = 3, name = (null)}
14:57:03 已经解锁 : taskB === <NSThread: 0x604000464580>{number = 4, name = (null)}
可以看到:pthread_mutex_t
实现了 NSRecursiveLock
相同效果的递归锁功能。
在这里,主要利用pthread_mutexattr_t
实现了递归锁的功能,我们来总结下pthread_mutexattr_t
的简单用法:
方法 | 方法描述 |
---|---|
int pthread_mutexattr_init(pthread_mutexattr_t *) |
初始化 |
int pthread_mutexattr_destroy(pthread_mutexattr_t *) |
销毁 |
int pthread_mutexattr_settype(pthread_mutexattr_t *, int) |
设置属性值 |
int pthread_mutexattr_gettype(const pthread_mutexattr_t * __restrict,int * __restrict) |
获取属性值 |
属性值 | 属性值描述 |
---|---|
PTHREAD_PROCESS_SHARE |
指定pthread_mutex_t 的范围:进程间的同步 |
PTHREAD_PROCESS_PRIVATE |
指定pthread_mutex_t 的范围:进程内线程间的同步(默认为进程内使用锁) |
PTHREAD_MUTEX_TIMED_NP |
这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性 |
PTHREAD_MUTEX_RECURSIVE |
如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE ) |
PTHREAD_MUTEX_RECURSIVE_NP |
同PTHREAD_MUTEX_RECURSIVE
|
PTHREAD_MUTEX_ERRORCHECK |
这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码 |
PTHREAD_MUTEX_ERRORCHECK_NP |
检错锁,如果同一个线程请求同一个锁,则返回EDEADLK ,否则与PTHREAD_MUTEX_TIMED_NP 类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁 |
PTHREAD_MUTEX_ADAPTIVE_NP |
适应锁,动作最简单的锁类型,仅等待解锁后重新竞争 |
PTHREAD_MUTEX_NORMAL |
这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果 |
PTHREAD_MUTEX_DEFAULT |
这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起不可预料的结果。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。POSIX标准规定,对于某一具体的实现,可以把这种类型的互斥锁定义为其他类型的互斥锁 |
3、条件锁
条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行
3.1、NSConditionLock
条件锁
NSConditionLock 是可以与特定的、用户定义的条件相关联的锁为条件锁。使用NSConditionLock
对象,可以确保只有满足特定条件时,线程才能获得锁。一旦它获得了锁并执行了代码的关键部分,线程就可以放弃锁并将相关条件设置为新的内容。条件本身是任意的,根据应用程序的需要定义它们。
我们来看一段程序:
- (void)conditionLockMethod
{
NSLog(@"开始处理 ------------ %@",NSThread.currentThread);
__block int length = 0;
//初始化一个条件锁,设置解锁条件为 1
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:1];
//将 taskA 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskA === 开始尝试加锁");
[conditionLock lock];//给 taskA 加锁
NSLog(@"taskA === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskA === length --- %d == %@",--length,NSThread.currentThread);
[conditionLock unlock];//taskA 完成后解锁
NSLog(@"taskA --- 已经解锁");
});
//将 taskB 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskB === 开始尝试加锁");
if ([conditionLock tryLockWhenCondition:1])//不会阻塞线程
{
NSLog(@"taskB === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskB === length --- %d == %@",--length,NSThread.currentThread);
[conditionLock unlock];//taskB 完成后解锁
NSLog(@"taskB --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskB === length --- %d == %@",length,NSThread.currentThread);
}
});
//将 taskC 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskC === 开始尝试加锁");
if ([conditionLock lockWhenCondition:1 beforeDate:[NSDate dateWithTimeIntervalSinceNow:4]])
{
NSLog(@"taskC === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskC === length --- %d == %@",--length,NSThread.currentThread);
[conditionLock unlockWithCondition:5];//taskC 完成后解锁
NSLog(@"taskC --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskC === length --- %d == %@",length,NSThread.currentThread);
}
});
//将 taskD 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskD === 开始尝试加锁");
[conditionLock lockWhenCondition:3];
NSLog(@"taskD === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskD === length --- %d == %@",--length,NSThread.currentThread);
[conditionLock unlockWithCondition:1];//taskE 完成后解锁
NSLog(@"taskD --- 已经解锁");
});
//延迟 10s 之后执行下述任务,等待前面所有任务全部执行完毕,确保 length 资源没有被加锁
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//将 taskE 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskE === 开始尝试加锁");
if ([conditionLock tryLockWhenCondition:3])
{
NSLog(@"taskE === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskE === length --- %d == %@",--length,NSThread.currentThread);
[conditionLock unlockWithCondition:1];//taskD 完成后解锁
NSLog(@"taskE --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskE === length --- %d == %@",length,NSThread.currentThread);
}
});
//将 taskF 放到一个并发队列中异步执行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"taskF === 开始尝试加锁");
if ([conditionLock tryLockWhenCondition:5])
{
NSLog(@"taskF === length === %d -- %@",++length,NSThread.currentThread);
[NSThread sleepForTimeInterval:2];//模拟耗时任务
NSLog(@"taskF === length --- %d == %@",--length,NSThread.currentThread);
[conditionLock unlockWithCondition:3];//taskF 完成后解锁
NSLog(@"taskF --- 已经解锁");
}
else
{
NSLog(@"加锁失败 :taskF === length --- %d == %@",length,NSThread.currentThread);
}
});
});
NSLog(@"结束处理 ------------ %@",NSThread.currentThread);
}
上述代码里有 taskA
、taskB
... taskF
总计六个任务,我们尝试以四种方法分别为这六个任务加锁操作:
-
taskA
使用- (void)lock;
方法加锁操作,使用- (void)unlock;
方法解锁 -
taskB
、taskE
、taskF
使用- (BOOL)tryLockWhenCondition:(NSInteger)condition;
方法加锁操作; -
taskC
使用- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
方法加锁;使用- (void)unlockWithCondition:(NSInteger)condition;
方法解锁; -
taskD
使用- (void)lockWhenCondition:(NSInteger)condition;
方法加锁
我们来看控制台输出语句:
09:51:46 taskA === 开始尝试加锁
09:51:46 taskB === 开始尝试加锁
09:51:46 taskC === 开始尝试加锁
09:51:46 taskD === 开始尝试加锁
09:51:46 taskA === length === 1 -- <NSThread: 0x60000027a800>{number = 3, name = (null)}
09:51:46 加锁失败 :taskB === length --- 1 == <NSThread: 0x604000663b80>{number = 4, name = (null)}
09:51:48 taskA === length --- 0 == <NSThread: 0x60000027a800>{number = 3, name = (null)}
09:51:48 taskA --- 已经解锁
09:51:48 taskC === length === 1 -- <NSThread: 0x6000002795c0>{number = 5, name = (null)}
09:51:50 taskC === length --- 0 == <NSThread: 0x6000002795c0>{number = 5, name = (null)}
09:51:50 taskC --- 已经解锁
09:51:56 taskE === 开始尝试加锁
09:51:56 taskF === 开始尝试加锁
09:51:56 加锁失败 :taskE === length --- 1 == <NSThread: 0x60400027f180>{number = 7, name = (null)}
09:51:56 taskF === length === 1 -- <NSThread: 0x600000277a00>{number = 6, name = (null)}
09:51:58 taskF === length --- 0 == <NSThread: 0x600000277a00>{number = 6, name = (null)}
09:51:58 taskF --- 已经解锁
09:51:58 taskD === length === 1 -- <NSThread: 0x604000665e00>{number = 8, name = (null)}
09:52:00 taskD === length --- 0 == <NSThread: 0x604000665e00>{number = 8, name = (null)}
09:52:00 taskD --- 已经解锁
数据 length
在多线程中操作被加锁之后是线程安全的,这方便不过多讨论;我们主要讨论下NSConditionLock
的几个方法执行特点:
- 从
taskA
来看: 执行NSLocking
协议的 一对加锁/解锁方法 同样使用; - 从
taskB
的打印时间可以看到:taskB
在condition
符合的条件下任然加锁失败,是因为这时taskA
已执行加锁操作,而- (BOOL)tryLockWhenCondition:(NSInteger)condition;
不会阻塞当前线程的执行,不能加锁直接返回NO
,然后去执行后续代码; - 从
taskC
的打印时间可以看到:taskC
开始加锁临界区后一直阻塞当前线程的执行,一直到2s
后taskA
执行完毕,被解锁后,taskC
才得以执行;我们注意到:taskC
使用- (void)unlockWithCondition:(NSInteger)condition;
方法解锁,那么这个condition
的设置到底是什么用处呢?我们接着往下分析 - 在
taskC
解锁之后6s
之后,taskE
、taskF
开始执行加锁操作:
理论来说:按照任务的先后顺序,应该是taskE
先执行,然后taskF
去执行加锁操作而由于taskE
已加锁导致taskF
加锁失败
但是taskE
加锁失败,taskF
加锁成功?这是为什么呢?
我们在taskC
任务执行完毕之后使用- (void)unlockWithCondition:(NSInteger)condition;
方法将condition
设置为5
,这充分阐释了NSConditionLock
条件锁的条件意义:只有条件合适,才能加锁成功。taskC
执行完毕,将我们初始化NSConditionLock
的条件1
改为了5
,而taskE
加锁条件是3
,不满足加锁条件,所以加锁失败直接返回NO
,不堵塞线程;
taskF
满足加锁条件,加锁成功,进去操作公共数据; - 到了最后:
taskD
开始操作数据了,我们可以看到从taskD
到临界区 到 执行任务,中间间隔了12s
,这也就是说:- (void)lockWhenCondition:(NSInteger)condition;
方法会堵塞当前线程的执行,一直等待条件满足,才会接着向下执行代码,由于我们在taskF
执行完毕解锁时设置条件为3
,这时taskD
监测到条件满足了,就开始向下执行了
总结 NSConditionLock
的几种加锁/解锁方法:
加锁/解锁方法 | 方法描述 |
---|---|
- (void)lock; |
会阻塞当前线程的执行,直到前面的任务完成解锁后,才会接着执行下面代码; |
- (void)unlock; |
解锁,不会改变 NSConditionLock 中的condition
|
- (BOOL)tryLock; |
不会阻塞当前线程的执行,不能加锁返回 NO /能加锁直接返回 YES ,然后去执行后续代码 |
- (BOOL)tryLockWhenCondition:; |
不会阻塞当前线程的执行,要加锁成功需要满足两个条件:其一为其余线程没有加锁,其二是condition 满足;只要满足这两条,才能加锁成功返回YES ,否则返回NO ; |
- (BOOL)lockBeforeDate:(NSDate *)limit; |
在所指定Date 之前尝试加锁,会阻塞线程,如果在指定时间之前都不能加锁,则返回 NO ,指定时间之前能加锁,则返回YES 。 |
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit; |
在所指定 Date 之前尝试加锁,会阻塞线程;如果在这个时间段内其余线程没有加锁而且满足条件 condition ,才会加锁成功;如果在等待的这段时间内:被其他线程加锁 或者 condition 条件不满足,就无法加锁成功。到等待之间之后,还不行,则返回NO ,加锁失败 |
- (void)unlockWithCondition:(NSInteger)condition; |
解锁,重置 NSConditionLock 中的condition 条件 |
3.2、NSCondition
条件锁
NSCondition 实现了一个线程在等待信号而阻塞时,可以被另外一个线程唤醒的功能。
NSCondition
的对象实际上是作为一个锁和线程检查器:锁主要是为了检测条件时保护数据源,执行条件引发的任务; 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
NSCondition
同样实现了NSLocking
协议,所以它和NSLock
一样,也可以调用lock
和unlock
方法,可以当做NSLock
来使用解决线程同步问题,用法完全一样。
我们不妨写个例子感受下NSCondition
的用法:
- (void)conditionMethod
{
NSCondition *condition = [[NSCondition alloc] init];
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"开始加锁 : taskA === %@",NSThread.currentThread);
[condition lock];//锁定条件对象。
[array addObject:@1];
while ([array containsObject:@5] == NO)//测试是否可以安全的履行接下来的任务。
{
NSLog(@"执行wait: taskA === %@",NSThread.currentThread);
//在调用 wait 方法前,必须调用 lock 给当前线程上锁
//当一个线程执行 wait 时,NSCondition 对象 解开锁 并在此处阻塞当前线程
//当 NSCondition 发送 signal 或者 broadcast 信号时唤醒线程
[condition wait];//如果布尔值是假的,调用条件对象的 wait 方法来阻塞线程,不会继续执行,只有等到 signal 或 broadcast 信号,才会接着执行
NSLog(@"接signal: taskA === %@",NSThread.currentThread);
}
[NSThread sleepForTimeInterval:3];//模拟耗时任务
[condition broadcast];
[condition unlock];//当任务完成后,解锁条件对象
NSLog(@"已经解锁 : taskA === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
[condition lock];
while ([array containsObject:@5] == NO)
{
NSLog(@"执行wait: taskB === %@",NSThread.currentThread);
[condition wait];
NSLog(@"接signal: taskB === %@",NSThread.currentThread);
}
[array addObject:@2];
[condition unlock];
NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskC === %@",NSThread.currentThread);
[condition lock];
while ([array containsObject:@5] == NO)
{
NSLog(@"执行wait: taskC === %@",NSThread.currentThread);
[condition wait];
NSLog(@"接signal: taskC === %@",NSThread.currentThread);
}
[array addObject:@4];
[condition unlock];
NSLog(@"已经解锁 : taskC === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskD === %@",NSThread.currentThread);
[condition lock];
while ([array containsObject:@4] == NO)
{
NSLog(@"执行wait: taskD === %@",NSThread.currentThread);
if ([condition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]])
{
NSLog(@"接signal: taskD === %@",NSThread.currentThread);
}
else
{
NSLog(@"taskD ---- 指定时间内也没有收到 信号");
}
}
[array addObject:@3];
[condition unlock];
NSLog(@"已经解锁 : taskD === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskF === %@",NSThread.currentThread);
[condition lock];
[array addObject:@5];
[condition signal];
NSLog(@"发送信号 : taskF === %@",NSThread.currentThread);
[condition unlock];
NSLog(@"已经解锁 : taskF === %@",NSThread.currentThread);
});
}
上述程序执行了taskA
、taskB
、... taskF
六个任务:
-
taskA
、taskB
、taskC
在执行上锁操作保护数据之后,根据数据条件决定接续往下执行还是执行-(void)wait;
-
taskD
在执行上锁操作保护数据之后,根据数据条件决定接续往下执行还是调用-(BOOL)waitUntilDate:(NSDate *)limit;
方法 -
taskF
在5s
后执行,执行添加数字5的任务,添加数字5之后发送signal
信号
我们来看下控制台输出:
14:25:37 开始加锁 : taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:37 执行wait: taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:38 开始加锁 : taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:38 执行wait: taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:38 开始加锁 : taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:38 执行wait: taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:39 开始加锁 : taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:39 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:40 taskD ---- 指定时间内也没有收到 信号
14:25:40 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:41 taskD ---- 指定时间内也没有收到 信号
14:25:41 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:42 taskD ---- 指定时间内也没有收到 信号
14:25:42 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:42 开始加锁 : taskF === <NSThread: 0x60000027b480>{number = 7, name = (null)}
14:25:42 发送信号 : taskF === <NSThread: 0x60000027b480>{number = 7, name = (null)}
14:25:44 已经解锁 : taskF === <NSThread: 0x60000027b480>{number = 7, name = (null)}
14:25:44 接signal: taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:47 taskD ---- 指定时间内也没有收到 信号
14:25:47 已经解锁 : taskA === <NSThread: 0x60000046ac40>{number = 3, name = (null)}
14:25:47 执行wait: taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
14:25:47 接signal: taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:47 已经解锁 : taskB === <NSThread: 0x604000279280>{number = 4, name = (null)}
14:25:47 接signal: taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:47 已经解锁 : taskC === <NSThread: 0x600000468a80>{number = 5, name = (null)}
14:25:48 taskD ---- 指定时间内也没有收到 信号
14:25:48 已经解锁 : taskD === <NSThread: 0x60400007d340>{number = 6, name = (null)}
通过运行程序可以看到:
- 首先: 由于数组
array
未包含5
,因此taskA
、taskB
、taskC
都在执行-(void)wait
后,线程阻塞,无法接着执行;
而taskD
由于执行-(BOOL)waitUntilDate:(NSDate *)limit;
方法,每到指定的limit
时间后,自动唤醒线程,判定条件; - 接着:我们在
taskF
中向数组array
添加了5
,执行-(void)signal;
方法。此时taskA
、taskB
、taskC
所在的线程被堵塞,等待signal
信号; 但是根据taskF
发送signal
的时间,以及处于等待的线程接收signal
的时间可以看出,只有taskA
接收到了taskF
发送的signal
信号,这证明了signal
只是一个信号量,只能唤醒一个等待的线程;
我们在taskA
在条件通过时阻塞了3s
执行了-(void)broadcast;
可以看到在3s
后,taskB
、taskC
线程先后被唤醒,这说明-(void)broadcast;
可以唤醒所有在等待的线程 - 最后,由于我们在
taskC
被唤醒之后向数组array
添加了4
,使得taskD
判定条件通过,taskD
也得以执行完毕。
总结:
方法 | 方法描述 |
---|---|
-(void)wait; |
会使线程一直处于休眠状态,直到收到调用 -(void)signal; 发出的 signal 为止; |
-(BOOL)waitUntilDate:(NSDate *)limit; |
在使线程睡眠的同时会设置睡眠的终止时间,如果在终止时间前收到了 signal 就会唤醒线程;当到达终止时间的时候,即使没有收到 signal ,也会直接唤醒线程,而不会像 -(void)wait; 那样一直睡眠下去 |
-(void)signal; |
signal 只是一个信号量,只能唤醒一个等待的线程,想唤醒多个就得多次调用;如果没有等待的线程,则这个方法不起作用; |
-(void)broadcast; |
可以唤醒所有在等待的线程。如果没有等待的线程,这个方法没有作用。 |
3.3、利用pthread_mutex_t
实现条件锁
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
- (void)pthread_mutex_ConditionMethod
{
__block pthread_mutex_t condMutexLock;
__block pthread_cond_t pthreadCondition;//条件变量
pthread_mutex_init(&condMutexLock, NULL);//初始化一个互斥锁
pthread_cond_init(&pthreadCondition, NULL);//初始化一个条件变量
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"开始加锁 : taskA === %@",NSThread.currentThread);
pthread_mutex_lock(&condMutexLock);//上锁
[array addObject:@1];
//wait接到signal后,并不意味着条件的值一定发生了变化,必须重新检查条件的值。最好的测试方法是循环调用:
while ([array containsObject:@5] == NO)//测试是否可以安全的履行接下来的任务。
{
NSLog(@"执行wait: taskA === %@",NSThread.currentThread);
pthread_cond_wait(&pthreadCondition, &condMutexLock);
NSLog(@"接signal: taskA === %@",NSThread.currentThread);
}
[NSThread sleepForTimeInterval:3];//模拟耗时任务
pthread_cond_broadcast(&pthreadCondition);
pthread_mutex_unlock(&condMutexLock);//解锁
NSLog(@"已经解锁 : taskA === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskB === %@",NSThread.currentThread);
pthread_mutex_lock(&condMutexLock);//上锁
while ([array containsObject:@5] == NO)
{
NSLog(@"执行wait: taskB === %@",NSThread.currentThread);
pthread_cond_wait(&pthreadCondition, &condMutexLock);
NSLog(@"接signal: taskB === %@",NSThread.currentThread);
}
[array addObject:@2];
pthread_mutex_unlock(&condMutexLock);//解锁
NSLog(@"已经解锁 : taskB === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskC === %@",NSThread.currentThread);
pthread_mutex_lock(&condMutexLock);//上锁
while ([array containsObject:@5] == NO)
{
NSLog(@"执行wait: taskC === %@",NSThread.currentThread);
pthread_cond_wait(&pthreadCondition, &condMutexLock);
NSLog(@"接signal: taskC === %@",NSThread.currentThread);
}
[array addObject:@4];
pthread_mutex_unlock(&condMutexLock);//解锁
NSLog(@"已经解锁 : taskC === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskD === %@",NSThread.currentThread);
pthread_mutex_lock(&condMutexLock);//上锁
while ([array containsObject:@4] == NO)
{
NSLog(@"执行wait: taskD === %@",NSThread.currentThread);
struct timespec abstime;
struct timeval now;
long timeout_s = 1; // 等待 1s
gettimeofday(&now, NULL);
long nsec = now.tv_usec * 1000 + timeout_s * 1000000;
abstime.tv_sec=now.tv_sec + nsec / 1000000000 + timeout_s;
abstime.tv_nsec=nsec % 1000000000;
if (pthread_cond_timedwait(&pthreadCondition, &condMutexLock, &abstime) == 0)
{
NSLog(@"接signal: taskD === %@",NSThread.currentThread);
}
else
{
NSLog(@"taskD ---- 指定时间内也没有收到 信号");
}
}
[array addObject:@3];
pthread_mutex_unlock(&condMutexLock);//解锁
NSLog(@"已经解锁 : taskD === %@",NSThread.currentThread);
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"开始加锁 : taskF === %@",NSThread.currentThread);
pthread_mutex_lock(&condMutexLock);//上锁
[array addObject:@5];
pthread_cond_signal(&pthreadCondition);
NSLog(@"发送信号 : taskF === %@",NSThread.currentThread);
pthread_mutex_unlock(&condMutexLock);//解锁
NSLog(@"已经解锁 : taskF === %@",NSThread.currentThread);
});
}
上述程序与3.2中的程序步骤一样,只是使用互斥锁pthread_mutex_t
和条件pthread_cond_t
替换了 NSCondition
。此执行了taskA
、taskB
、... taskF
六个任务:
-
taskA
、taskB
、taskC
在执行上锁操作保护数据之后,根据数据条件决定接续往下执行还是执行pthread_cond_wait()
; -
taskD
在执行上锁操作保护数据之后,根据数据条件决定接续往下执行还是调用pthread_cond_timedwait()
方法 -
taskF
在5s
后执行,执行添加数字5的任务,添加数字5
之后发送pthread_cond_signal()
信号
我们来看下控制台输出:
10:16:32 开始加锁 : taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:32 执行wait: taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:33 开始加锁 : taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:33 执行wait: taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:33 开始加锁 : taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:33 执行wait: taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:34 开始加锁 : taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:34 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:35 taskD ---- 指定时间内也没有收到 信号
10:16:35 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:36 taskD ---- 指定时间内也没有收到 信号
10:16:36 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:37 taskD ---- 指定时间内也没有收到 信号
10:16:37 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:37 开始加锁 : taskF === <NSThread: 0x604000465540>{number = 7, name = (null)}
10:16:37 发送信号 : taskF === <NSThread: 0x604000465540>{number = 7, name = (null)}
10:16:37 接signal: taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:37 已经解锁 : taskF === <NSThread: 0x604000465540>{number = 7, name = (null)}
10:16:40 taskD ---- 指定时间内也没有收到 信号
10:16:40 已经解锁 : taskA === <NSThread: 0x60400046b4c0>{number = 3, name = (null)}
10:16:40 执行wait: taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
10:16:41 接signal: taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:41 已经解锁 : taskB === <NSThread: 0x60400046d080>{number = 4, name = (null)}
10:16:41 接signal: taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:41 已经解锁 : taskC === <NSThread: 0x600000464000>{number = 5, name = (null)}
10:16:42 taskD ---- 指定时间内也没有收到 信号
10:16:42 已经解锁 : taskD === <NSThread: 0x600000277440>{number = 6, name = (null)}
通过打印可以看到:替换NSCondition
之后的代码执行与使用NSCondition
的代码执行,效果完全一样;
- 首先: 由于数组
array
未包含5
,因此taskA
、taskB
、taskC
都在执行pthread_cond_wait()
后,线程阻塞,无法接着执行;而taskD
由于执行pthread_cond_timedwait()
方法,每到指定的时间后,自动唤醒线程,判定条件; - 接着:我们在
taskF
中向数组array
添加了5
,执行pthread_cond_signal()
方法。此时taskA
、taskB
、taskC
所在的线程被堵塞,等待signal
信号; 但是根据taskF
发送signal
的时间,以及处于等待的线程接收signal
的时间可以看出,只有taskA
接收到了taskF
发送的signal
信号,这证明了signal
只是一个信号量,只能唤醒一个等待的线程;
我们在taskA
在条件通过时阻塞了3s
执行了pthread_cond_broadcast()
可以看到在3s
后,taskB
、taskC
线程先后被唤醒,这说明pthread_cond_broadcast()
可以唤醒所有在等待的线程; - 最后,由于我们在
taskC
被唤醒之后向数组array
添加了4
,使得taskD
判定条件通过,taskD
也得以执行完毕。
总结:
方法 | 方法描述 |
---|---|
int pthread_cond_init (pthread_cond_t* cond, pthread_condattr_t *cond_attr) |
初始化一个条件变量 |
宏PTHREAD_COND_INITIALIZER
|
静态创建一个条件变量 |
int pthread_cond_destroy(pthread_cond_t* cond) |
销毁一个条件变量 |
int pthread_cond_wait(pthread_cond_t * __restrict,pthread_mutex_t * __restrict) |
无条件等待; |
int pthread_cond_timedwait(pthread_cond_t * __restrict, pthread_mutex_t * __restrict,const struct timespec * _Nullable __restrict) |
计时等待;当在指定时间内有信号传过来时,此方法返回0 ,否则返回一个非0 数 |
int pthread_cond_signal(pthread_cond_t* cond) |
激发条件,激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个 |
int pthread_cond_broadcast(pthread_cond_t* cond) |
激发条件,激活所有等待线程 |
关于等待条件
等待条件有两种方式:无条件等待pthread_cond_wait()
和计时等待pthread_cond_timedwait()
,其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT
,结束等待;
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()
或pthread_cond_timedwait()
的竞争条件Race Condition
。pthread_mutex_t
互斥锁必须是普通锁PTHREAD_MUTEX_TIMED_NP
或者适应锁PTHREAD_MUTEX_ADAPTIVE_NP
,且在调用pthread_cond_wait()
或pthread_cond_timedwait()
前必须由本线程加锁pthread_mutex_lock()
,而在更新条件等待队列以前,pthread_mutex_t
保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()
之前,pthread_mutex_t()
将被重新加锁,以与进入pthread_cond_wait()
前的加锁动作对应。
4、信号量
信号量的本质是数据操作锁, 它本身不具有数据交换的功能,而是通过控制其他的通信资源来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。
4.1、dispatch_semaphore
信号量
dispatch_semaphore
和 NSCondition
类似,都是一种基于信号的同步方式。但 NSCondition
信号只能发送,不能保存(如果没有线程在等待,则发送的信号会失效);而 dispatch_semaphore
能保存发送的信号。dispatch_semaphore
的核心是 dispatch_semaphore_t
类型的信号量。
我们可以使用 NSOperationQueue
来直接控制线程的最大并发数量,但是我们如何在 dispatch_queue
中控制线程的最大并发数?可以利用 GCD 的 dispatch_semaphore_t
达到控制线程的最大并发数的目的!我们来看一段代码:
- (void)dispatch_semaphoreMethod
{
dispatch_queue_t queue = dispatch_queue_create("com.demo.task", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
NSLog(@"-------- 开始处理 -------");
dispatch_apply(6, queue, ^(size_t i) {
long single = dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 只有当信号量大于 0 的时候,线程将信号量减 1,程序向下执行
// 否则线程会阻塞并且一直等待,直到信号量大于 0
dispatch_group_async(group, queue, ^{
NSLog(@"开始执行第 %zu 次任务 == single : %ld----- %@",i,single,NSThread.currentThread);
[NSThread sleepForTimeInterval:(i % 2 == 0 ? 2 : 3)];//模拟耗时任务
long value = dispatch_semaphore_signal(semaphore);// 当线程任务执行完成之后,发送一个信号,增加信号量。
NSLog(@"结束执行第 %zu 次任务 == single : %ld----- %@",i,value,NSThread.currentThread);
});
});
//既控制了线程数量,也在执行任务完成之后得到了通知。
dispatch_group_notify(group, queue, ^{
NSLog(@"任务结束 ------ %@",NSThread.currentThread);
});
}
在上述程序中,我们利用 dispatch_apply
并发迭代了六次,向 dispatch_group
中增加了六个耗时任务;考虑到性能的优化,我们不希望无限制的开辟线程执行任务;这时可以使用 dispatch_semaphore_t
控制了线程的并发数量;
我们来看下打印结果:
10:37:32 -------- 开始处理 -------
10:37:32 开始执行第 0 次任务 == single : 0----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:32 开始执行第 1 次任务 == single : 0----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:34 结束执行第 0 次任务 == single : 1----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:34 开始执行第 2 次任务 == single : 0----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:35 结束执行第 1 次任务 == single : 1----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:35 开始执行第 3 次任务 == single : 0----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:36 结束执行第 2 次任务 == single : 1----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:36 开始执行第 4 次任务 == single : 0----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:38 开始执行第 5 次任务 == single : 0----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:38 结束执行第 3 次任务 == single : 1----- <NSThread: 0x60400046a280>{number = 3, name = (null)}
10:37:38 结束执行第 4 次任务 == single : 0----- <NSThread: 0x600000264380>{number = 4, name = (null)}
10:37:41 结束执行第 5 次任务 == single : 0----- <NSThread: 0x604000462800>{number = 5, name = (null)}
10:37:41 任务结束 ------ <NSThread: 0x604000462800>{number = 5, name = (null)}
我们可以看到: dispatch_group
中使用的线程只有两条。 因为我们使用 dispatch_semaphore_create()
创建了 dispatch_semaphore_t
,并赋予初值为 2
。
- 我们在将任务添加至
dispatch_group
之前执行dispatch_semaphore_wait()
方法,减少一个信号;在任务执行结束之后,执行dispatch_semaphore_signal()
方法,增加一个信号。 - 当执行第
1
次任务时,dispatch_semaphore
信号已经减少为0
,此时dispatch_semaphore_wait()
方法无法再减少信号继续执行,只能堵塞当前线程,导致第2
次任务无法开始执行; -
开始处理 2s
之后当第0
次任务执行完毕,调用dispatch_semaphore_signal()
方法,增加一个信号,此时执行wait
的临界区唤醒线程执行后续代码,开始执行任务2
,dispatch_semaphore
信号再次减少为0
; -
开始处理3s
之后当第1
次任务执行完毕,调用dispatch_semaphore_signal()
方法,增加一个信号,此时执行wait
的临界区唤醒线程执行后续代码,开始执行任务3
,dispatch_semaphore
信号再次减少为0
; - 后续的任务以此类推... 直至所有任务执行完毕。
从上面的代码可以看到,一个 dispatch_semaphore_wait(signal, overTime);
方法会去对应一个 dispatch_semaphore_signal(signal);
看起来像NSLock
的lock
和 unlock
,其实可以这样理解,区别只在于有信号量这个参数,lock
、unlock
只能同一时间,一个线程访问被保护的临界区,而如果 dispatch_semaphore
的信号量初始值为 value
,则可以有value
个线程同时访问被保护的临界区。
总结:
dispatch_semaphore 方法 |
方法描述 |
---|---|
dispatch_semaphore_create(long value) |
创建一个 dispatch_semaphore_t 类型的信号量,设定信号量的初始值为 value 。注意:这里的传入的参数必须大于或等于 0 ,否则 dispatch_semaphore_create 会返回NULL
|
dispatch_semaphore_wait(signal, overTime); |
判断signal 的信号值value 是否大于0 。大于 0 不会阻塞线程,value 减1 ,执行后续任务。如果信号值value 为 0 ,该线程会和 NSCondition 一样直接进入 waiting 状态,等待其他线程发送信号唤醒线程去执行后续任务,或者当 overTime 时限到了,也会执行后续任务。 |
dispatch_semaphore_signal(signal); |
使value 值加一,发送信号,唤醒wait 中的线程。 |
5、读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。
5.1、dispatch_barrier_async
实现读写锁
- (void)dispatch_barrier_Method
{
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t queue = dispatch_queue_create("com.demo.barrier", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始执行写入操作");
dispatch_async(queue, ^{
NSLog(@"开始执行taskA === %@",NSThread.currentThread);
for (int i = 0; i < 100; i ++)
{
[array addObject:@(i)];
}
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskA === %@",NSThread.currentThread);
});
//DISPATCH_BLOCK_BARRIER 保证代码块用于原子性,代码块的代码未执行结束前,下一次调用将进入一个FIFO的等待队列,等待本次代码块执行结束,使用较为安全
dispatch_block_t taskC = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"开始执行taskC === %@",NSThread.currentThread);
NSLog(@"taskC读取数据---- %@",array[189]);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskC === %@",NSThread.currentThread);
});
dispatch_block_t taskD = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"开始执行taskD === %@",NSThread.currentThread);
NSLog(@"taskD读取数据---- %@",array[10]);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskD === %@",NSThread.currentThread);
});
NSLog(@"开始执行判定操作");
dispatch_barrier_async(queue, ^{
NSLog(@"开始执行taskB === %@",NSThread.currentThread);
if (array.count < 100){
//dispatch_block_cancel() 取消执行某个block,只有当block还未执行前执行cancel有效,block正在执行无法取消.
dispatch_block_cancel(taskC);
dispatch_block_cancel(taskD);
}else if (array.count < 200){
dispatch_block_cancel(taskC);
}
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskB === %@",NSThread.currentThread);
});
NSLog(@"开始执行读取操作");
dispatch_async(queue, taskC);
dispatch_async(queue, taskD);
NSLog(@"结束执行读写操作");
}
我们创建了一个共享资源array
,在 taskA
里对array
执行写入操作;利用异步栅栏执行阻隔操作,等待taskA
写入操作完成,再执行dispatch_barrier_async()
的block
中的任务taskB
,taskB
是对共享资源array
的判定,决定后续是否执行taskC
与taskD
。
我们来看以下打印结果:
10:01:10 开始执行写入操作
10:01:10 开始执行判定操作
10:01:10 开始执行taskA === <NSThread: 0x604000267e00>{number = 3, name = (null)}
10:01:10 开始执行读取操作
10:01:10 结束执行读写操作
10:01:13 结束执行taskA === <NSThread: 0x604000267e00>{number = 3, name = (null)}
10:01:13 开始执行taskB === <NSThread: 0x604000267e00>{number = 3, name = (null)}
10:01:16 结束执行taskB === <NSThread: 0x604000267e00>{number = 3, name = (null)}
10:01:16 开始执行taskD === <NSThread: 0x604000267e00>{number = 3, name = (null)}
10:01:16 taskD读取数据---- 10
10:01:19 结束执行taskD === <NSThread: 0x604000267e00>{number = 3, name = (null)}
- 通过
开始执行写入操作
、开始执行判定操作
、开始执行读取操作
、结束执行读写操作
的打印时间,可以了解到dispatch_barrier_async()
并不会阻塞当前线程,而且dispatch_barrier_async()
中的block
的代码在任意分线程执行; -
taskA
写入执行完毕,开始执行taskB
;由于在taskA
中写入100
个数据,所以taskB
判定taskC
不能执行,只能执行taskD
; -
taskB
判定执行完毕之后,开始执行taskD
读取操作
5.2、dispatch_barrier_sync
实现读写锁
- (void)dispatch_barrier_Method
{
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t queue = dispatch_queue_create("com.demo.barrier", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始执行写入操作");
dispatch_async(queue, ^{
NSLog(@"开始执行taskA === %@",NSThread.currentThread);
for (int i = 0; i < 100; i ++)
{
[array addObject:@(i)];
}
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskA === %@",NSThread.currentThread);
});
//DISPATCH_BLOCK_BARRIER 保证代码块用于原子性,代码块的代码未执行结束前,下一次调用将进入一个FIFO的等待队列,等待本次代码块执行结束,使用较为安全
dispatch_block_t taskC = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"开始执行taskC === %@",NSThread.currentThread);
NSLog(@"taskC读取数据---- %@",array[189]);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskC === %@",NSThread.currentThread);
});
dispatch_block_t taskD = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
NSLog(@"开始执行taskD === %@",NSThread.currentThread);
NSLog(@"taskD读取数据---- %@",array[10]);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskD === %@",NSThread.currentThread);
});
NSLog(@"开始执行判定操作");
dispatch_barrier_sync(queue, ^{
NSLog(@"开始执行taskB === %@",NSThread.currentThread);
if (array.count < 100){
//dispatch_block_cancel() 取消执行某个block,只有当block还未执行前执行cancel有效,block正在执行无法取消.
dispatch_block_cancel(taskC);
dispatch_block_cancel(taskD);
}else if (array.count < 200){
dispatch_block_cancel(taskC);
}
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行taskB === %@",NSThread.currentThread);
});
NSLog(@"开始执行读取操作");
dispatch_async(queue, taskC);
dispatch_async(queue, taskD);
NSLog(@"结束执行读写操作");
}
我们创建了一个共享资源array
,在 taskA
里对array
执行写入操作;利用同步栅栏执行阻隔操作,等待taskA
写入操作完成,再执行dispatch_barrier_sync()
的block
中的任务taskB
,taskB
是对共享资源array
的判定,决定后续是否执行taskC
与taskD
。
我们来看以下打印结果:
10:02:36 开始执行写入操作
10:02:36 开始执行判定操作
10:02:36 开始执行taskA === <NSThread: 0x600000479440>{number = 3, name = (null)}
10:02:39 结束执行taskA === <NSThread: 0x600000479440>{number = 3, name = (null)}
10:02:39 开始执行taskB === <NSThread: 0x60400006f1c0>{number = 1, name = main}
10:02:42 结束执行taskB === <NSThread: 0x60400006f1c0>{number = 1, name = main}
10:02:42 开始执行读取操作
10:02:42 结束执行读写操作
10:02:42 开始执行taskD === <NSThread: 0x600000479440>{number = 3, name = (null)}
10:02:42 taskD读取数据---- 10
10:02:45 结束执行taskD === <NSThread: 0x600000479440>{number = 3, name = (null)}
- 通过
开始执行写入操作
、开始执行判定操作
、开始执行读取操作
、结束执行读写操作
的打印时间,可以了解到dispatch_barrier_sync()
会阻塞当前线程,只有dispatch_barrier_sync()
前面的代码与dispatch_barrier_sync()
中block
任务执行完毕,才会接着向下执行,而且dispatch_barrier_sync()
中block
的任务所处线程与上下文线程一样; -
taskA
写入执行完毕,开始执行taskB
;由于在taskA
中写入100
个数据,所以taskB
判定taskC
不能执行,只能执行taskD
; -
taskB
判定执行完毕之后,开始执行taskD
读取操作
dispatch_barrier_sync()
函数与dispatch_barrier_async()
函数的异同点:
dispatch_barrier_sync()
函数与dispatch_barrier_async()
函数的相同点:
- 将目标队列
dispatch_queue
一分为2,分割处设置同步点; - 等待目标队列
dispatch_queue
同步点之前的任务块先执行完毕,才会执行该函数的任务块; - 处理目标队列
dispatch_queue
同步点上的任务块完毕之后,接着处理目标队列dispatch_queue
同步点之后的任务;
dispatch_barrier_sync()
函数与dispatch_barrier_async()
函数的不同点:
dispatch_barrier_sync() 函数 |
dispatch_barrier_async() 函数 |
---|---|
堵塞当前线程 | 不会堵塞当前线程 |
不会开辟新的线程处理任务块 | 开辟新的线程处理任务块 |
5.3、pthread_rwlock_t
读写锁
读写锁pthread_rwlock_t
是用来解决读写操作问题的,读取数据操作可以共享,写入数据操作是排他的,读取数据可以有多个在读,,写入数据只有唯一个在写,同时写入数据的时候不允许读取数据。它具有强读者同步和强写者同步两种形式:
- 强写者同步:当所有写者都写完之后,才能进行读操作,读者需要最新的信息,一些事实性较高的系统可能会用到该所,比如定票之类的。
- 强读者同步:当写者没有进行写操作,读者就可以访问;
- (void)pthread_rwlock_Method
{
__block pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
NSMutableArray *array = [NSMutableArray array];
dispatch_queue_t queue = dispatch_queue_create("com.demo.barrier", DISPATCH_QUEUE_CONCURRENT);
//DISPATCH_BLOCK_DETACHED 不考虑线程安全
dispatch_block_t writeTask = dispatch_block_create(DISPATCH_BLOCK_BARRIER, ^{
pthread_rwlock_wrlock(&rwlock);
NSInteger index = array.count;
NSLog(@"开始执行writeTask:%ld === %@",index,NSThread.currentThread);
[array addObject:@(index)];
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行writeTask:%ld === %@",index,NSThread.currentThread);
pthread_rwlock_unlock(&rwlock);
});
NSLog(@"开始执行写入操作");
for (int i = 0; i < 3; i++) {
dispatch_async(queue, writeTask);
}
NSLog(@"开始执行读取操作");
for (NSInteger index = 0; index < 3; index++) {
dispatch_async(queue, ^{
pthread_rwlock_rdlock(&rwlock);
NSLog(@"开始执行readTask:%ld === %@",index,NSThread.currentThread);
NSLog(@"读取数据:%ld---- %@",index,array[index]);
[NSThread sleepForTimeInterval:3];//模拟耗时任务
NSLog(@"结束执行readTask:%ld === %@",index,NSThread.currentThread);
pthread_rwlock_unlock(&rwlock);
});
}
NSLog(@"结束执行读写操作");
}
我们创建了一个共享资源array
,首先对array
执行写入操作,加锁pthread_rwlock_wrlock()
; 接着读取共享资源array
,加锁pthread_rwlock_rdlock()
;我们来看以下打印结果:
10:24:45 开始执行写入操作
10:24:45 开始执行读取操作
10:24:45 开始执行writeTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:45 结束执行读写操作
10:24:48 结束执行writeTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:48 开始执行writeTask:1 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:51 结束执行writeTask:1 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:51 开始执行writeTask:2 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:54 结束执行writeTask:2 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:54 开始执行readTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:54 开始执行readTask:1 === <NSThread: 0x60400027cb80>{number = 4, name = (null)}
10:24:54 开始执行readTask:2 === <NSThread: 0x600000473600>{number = 5, name = (null)}
10:24:54 读取数据:0---- 0
10:24:54 读取数据:1---- 1
10:24:54 读取数据:2---- 2
10:24:57 结束执行readTask:2 === <NSThread: 0x600000473600>{number = 5, name = (null)}
10:24:57 结束执行readTask:0 === <NSThread: 0x60000046aec0>{number = 3, name = (null)}
10:24:57 结束执行readTask:1 === <NSThread: 0x60400027cb80>{number = 4, name = (null)}
通过开始执行写入操作
、开始执行读取操作
、结束执行读写操作
的打印时间,可以了解到pthread_rwlock_t()
不会阻塞当前线程;
通过writeTask
可以看到,pthread_rwlock_wrlock()
写入数据操作是排他的,只有唯一个在写,同时写入数据的时候不允许读取数据;
通过readTask
可以看到,pthread_rwlock_rdlock()
读取数据可以有多个在读
方法 | 方法描述 |
---|---|
int pthread_rwlock_init(pthread_rwlock_t * __restrict, const pthread_rwlockattr_t * _Nullable __restrict) |
初始化一个读写锁 |
int pthread_rwlock_destroy(pthread_rwlock_t * ) |
释放指定读写锁 |
int pthread_rwlock_wrlock(pthread_rwlock_t *) |
读写锁 锁定 写入数据操作,会堵塞线程 |
int pthread_rwlock_trywrlock(pthread_rwlock_t *) |
读写锁 锁定 写入数据操作,不会堵塞线程 |
int pthread_rwlock_rdlock(pthread_rwlock_t *) |
读写锁 锁定 读取数据操作,会堵塞线程 |
int pthread_rwlock_tryrdlock(pthread_rwlock_t *) |
读写锁 锁定 读取数据操作,不会堵塞线程 |
int pthread_rwlock_unlock(pthread_rwlock_t *) |
解锁读写锁 |
5.4、互斥锁与读写锁的区别:
当访问临界区资源时(访问的含义包括所有的操作:读和写),需要上互斥锁;
当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。
读写锁的优点:对于读数据比修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都要上互斥锁,而采用读写锁,则可以在任一时刻允许多个读出者存在,提高了更高的并发度,同时在某个写入者修改数据期间保护该数据,以免任何其它读出者或写入者的干扰。
6、Once
操作
6.1、dispatch_once_t
操作
- (void)dispatch_once_Method
{
static id shareInstance;
static dispatch_once_t onceToken;
dispatch_queue_t queue = dispatch_queue_create("com.demo.task", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(3, queue, ^(size_t index) {
NSLog(@"开始执行:%zu ====== %@",index,NSThread.currentThread);
dispatch_once(&onceToken, ^{
if (!shareInstance) {
shareInstance = [[NSObject alloc] init];
[NSThread sleepForTimeInterval:3];
NSLog(@"dispatch_once 执行内部 ====== %@",NSThread.currentThread);
}
});
NSLog(@"结束执行:%zu ====== %@",index,NSThread.currentThread);
});
}
打印结果:
14:05:17 开始执行:0 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:05:17 开始执行:1 ====== <NSThread: 0x600000471440>{number = 3, name = (null)}
14:05:17 开始执行:2 ====== <NSThread: 0x60000046ed40>{number = 4, name = (null)}
14:05:20 dispatch_once 执行内部 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:05:20 结束执行:0 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:05:20 结束执行:1 ====== <NSThread: 0x600000471440>{number = 3, name = (null)}
14:05:20 结束执行:2 ====== <NSThread: 0x60000046ed40>{number = 4, name = (null)}
如果在一个线程调用dispatch_once()
函数时,另外的线程调用dispatch_once
,则调用线程等待,直到首次调用dispatch_once()
的线程返回。
6.2、pthread_once_t
操作
void pthread_once_Function(void) {
static id shareInstance;
shareInstance = [[NSObject alloc] init];
[NSThread sleepForTimeInterval:3];
NSLog(@"pthread_once 执行内部 ====== %@",NSThread.currentThread);
}
- (void)pthread_once_Method
{
pthread_once_t once = PTHREAD_ONCE_INIT;
dispatch_queue_t queue = dispatch_queue_create("com.demo.task", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(3, queue, ^(size_t index) {
NSLog(@"开始执行:%zu ====== %@",index,NSThread.currentThread);
int onceValue = pthread_once(&once, &pthread_once_Function);
NSLog(@"结束执行:%zu ====== %d",index,onceValue);
});
}
打印结果:
14:06:03 开始执行:0 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:06:03 开始执行:1 ====== <NSThread: 0x60400046ee40>{number = 5, name = (null)}
14:06:03 开始执行:2 ====== <NSThread: 0x60000047ab80>{number = 6, name = (null)}
14:06:06 pthread_once 执行内部 ====== <NSThread: 0x60000007d2c0>{number = 1, name = main}
14:06:06 结束执行:0 ====== 0
14:06:06 结束执行:1 ====== 0
14:06:06 结束执行:2 ====== 0
类型为pthread_once_t
的变量是一个控制变量。控制变量必须使用PTHREAD_ONCE_INIT
宏静态地初始化。
pthread_once()
函数首先检查控制变量,判断是否已经完成初始化,如果完成就简单地返回;否则,pthread_once
调用初始化函数,并且记录下初始化被完成。如果在一个线程初始时,另外的线程调用pthread_once
,则调用线程等待,直到那个线程完成初始化返回。
7、属性的atomic/nonatomic
atomic/nonatomic
用来决定编译器生成的getter
和setter
是否为原子操作
7.1、atomic
设置成员变量的@property
属性时,默认为atomic
,系统会保证在其自动生成的 getter/setter
方法中的操作是完整的,不受其他线程的影响。例如线程5
在执行 getter
方法时,线程6
执行了 setter
方法,此时 线程5
依然会得到一个完整无损的对象。
//声明一个atomic修饰的属性
@property (atomic ,copy) NSString *testStrig;
//setter 方法的内部实现
- (void)setTestStrig:(NSString *)testStrig
{
{lock}
if (![_testStrig isEqualToString:testStrig])
{
_testStrig = testStrig;
}
{unlock}
}
atomic
不是线程安全的,如果有另一个线程9
同时在调[testStrig release]
,那可能就会crash,因为 release
不受 getter/setter
操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
atomic |
nonatomic |
---|---|
默认修饰符 | 不是默认的 |
读写速度慢,性能差 | 速度更快,提高性能 |
读写安全,线程不安全 | 读写不安全,线程不安全 |
参考文章:
iOS 常见知识点(三):Lock
C语言互斥锁pthread_mutex_t
iOS 中常见的几种锁
pthread_cond_wait()用法分析
pthread_rwlock_t读写锁函数说明
ios atomic nonatomic区别