之前在多线程——锁?中我有总结过锁,看本文之前可以先看看那篇文章。
@synchronized
我们知道,@synchronized是一把互斥锁(互斥锁保证在任何时刻都只能有一个线程访问该对象),它通过大括号来作为加锁、解锁的标识。
来看下面这个例子:
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketCount = 20;
[self lv_testSaleTicket];
}
//开4条多线程同时卖票
- (void)lv_testSaleTicket {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张", self.ticketCount);
} else {
NSLog(@"当前车票已售罄");
}
}
打印结果如下:
通过打印结果我们可以看出,这样是有问题的,线程不安全。我们加一把锁保证线程安全:
- (void)saleTicket {
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张", self.ticketCount);
} else {
NSLog(@"当前车票已售罄");
}
}
}
此时再打印,结果如下:
这时打印结果就正常了。
我们上例是通过@synchronized锁保证了线程的安全。接下来我们就来分析下@synchronized锁。
我们有两种方式开启研究。
第一种比较简单,就是汇编。首先在加锁的地方打个断点,如下图:
然后打开汇编:
就可以找到对应的关键字眼:
第二种方式就是使用如下指令将OC文件编译成C++代码:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
这种方式这里就不做演示了。
通过上面两种方式的分析,我们可以知道,@synchronized锁在底层是通过objc_sync_enter和objc_sync_exit函数来分别进行加锁和解锁的。
然后我下一个objc_sync_enter的符号断点:
发现objc_sync_enter是在objc库中,因此我就在libobjc源码中去查找objc_sync_enter:
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
好,接下来我们就来看一下@synchronized后面的对象不为空的情况:
这是正常情况下都会进行的操作,我们看到,首先是通过id2data函数来生成一个SyncData类型的对象。所以,我们先来看一下SyncData的定义:
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
再来看一下id2data这个函数的实现:
我们知道,无论是在Controller、View还是Model中,@synchronized锁都可以使用,可以说,@synchronized锁可以在程序的任何地方使用,因此@synchronized锁具备全局性。
而通过上面SyncData类型以及id2data函数源码,我们可以猜到,实际上在底层会将@synchronized锁住的那部分内容封装成一个SyncData类型的节点对象。
那么这些节点对象是怎么存储的呢?实际上它就是通过哈希表的形式进行存储SyncData类型的节点的,这张哈希表是一个全局的哈希表:
接下来我们看个例子:
该例中运行会崩溃。在Setter方法里面,会首先对_testArray进行release(可以参考内存管理之MRC来了解setter的自定义),然后才会再对_testArray进行赋值。如果是多条线程同时进行,那么就会有可能出现这样一种情形:_testArray指向的对象被销毁了,并且原对象的内存空间被复用了,但是_testArray还没有指向新的对象,此时呢又对_testArray执行了一次release,这个时候就会导致野指针调用。这就是上面?程序崩溃的原因所在。
那么如何完善呢?答案是加一把锁。
下面我们加一把@synchronized锁:
我们知道@synchronized锁后面跟的是一个对象,这个对象是作为锁的标识,我们这里将self.testArray作为@synchronized锁的唯一标识传递进去,然后运行:
程序也崩了!这是为啥呢?
上文中我有提到,如果@synchronized后面的对象传的是nil,那么将不会做任何事情,也就是说,@synchronized大括号里面包含的内容将不会被加锁。
根据前面的分析我们应该知道,self.testArray有可能是为nil,此时就不会加锁,因此问题就又回到了没有加锁的情形,所以在此崩溃也就顺理成章了。
通过这个问题呢,我其实是想说明一点,就是@synchronized这种加锁的形式实际上是不太靠谱的,它虽然使用简单,但是不太安全,一旦传入的对象为空,就会出现问题。
上面的情形,我们直接使用NSLock来处理:
此时就没什么问题了。
NSLock
上面?我们已经演示了NSLock的使用场景,接下来我们看一下NSLock的实现源码。
我点进NSLock这个类,发现它是Foundation框架下面的,而OC的Foundation没有开源,但是Swift的Foundation开源了一部分, 我们下载好swift-corelibs-foundation-master的源码,然后找到NSLock的实现:
open class NSLock: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
deinit {
#if os(Windows)
// SRWLocks do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
#endif
mutex.deinitialize(count: 1)
mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
}
open func lock() {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
open func `try`() -> Bool {
#if os(Windows)
return TryAcquireSRWLockExclusive(mutex) != 0
#else
return pthread_mutex_trylock(mutex) == 0
#endif
}
open func lock(before limit: Date) -> Bool {
#if os(Windows)
if TryAcquireSRWLockExclusive(mutex) != 0 {
return true
}
#else
if pthread_mutex_trylock(mutex) == 0 {
return true
}
#endif
#if os(macOS) || os(iOS) || os(Windows)
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
guard var endTime = timeSpecFrom(date: limit) else {
return false
}
return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
}
open var name: String?
}
可以看到,NSLock实际上就是对系统底层互斥锁的简单封装。
接下来看个例子:
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
运行之后打印结果如下:
2021-03-24 13:58:04.888859+0800 003-NSLock分析[7265:1263518] current value = 10
testMethod是一个递归函数,testMethod(10)应该打印十次才对,这里才打印了一次,这明显是不对的。
我们在递归函数中,应该使用递归锁。
NSRecursiveLock
紧接着上面那个例子,我们将NSLock替换为NSRecursiveLock:
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
然后再运行,结果如下:
2021-03-24 14:02:11.464604+0800 003-NSLock分析[7298:1270869] current value = 10
2021-03-24 14:02:11.464812+0800 003-NSLock分析[7298:1270869] current value = 9
2021-03-24 14:02:11.465040+0800 003-NSLock分析[7298:1270869] current value = 8
2021-03-24 14:02:11.465184+0800 003-NSLock分析[7298:1270869] current value = 7
2021-03-24 14:02:11.465322+0800 003-NSLock分析[7298:1270869] current value = 6
2021-03-24 14:02:11.465426+0800 003-NSLock分析[7298:1270869] current value = 5
2021-03-24 14:02:11.465581+0800 003-NSLock分析[7298:1270869] current value = 4
2021-03-24 14:02:11.465804+0800 003-NSLock分析[7298:1270869] current value = 3
2021-03-24 14:02:11.466708+0800 003-NSLock分析[7298:1270869] current value = 2
2021-03-24 14:02:11.467291+0800 003-NSLock分析[7298:1270869] current value = 1
此时打印正常了。
我们接下来再对上面的例子做一个改造:
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
实际上就是在外层加了一个for循环,然后我们运行发现崩溃了:
崩溃的原因就在于,死锁了!那么这个死锁是怎么产生的呢?且听我下面慢慢分析。
for循环执行了10次,因此会有10个异步添加到全局队列的操作,也就是会产生10条多线程来执行任务。
每一条多线程中都会有一个递归函数,递归函数中会有加锁操作。此时就很可能会发生下面的场景:线程1中通过lock加了锁,但是还没有unlock,此时线程2也通过lock加了锁,那么线程1和线程2就同时持有了某一块内存资源,并且他们两个线程对于自己已经持有的资源都不会主动放弃,也不会强势抢夺对方的资源,只能是互相等待,也就是说,线程1的unlock需要等待线程2的unlock,而线程2中的unlock又需要等待线程1 中的unlock执行完毕才能执行,这就导致了死锁。
那么针对这种情况应该怎么办呢?答案就是使用@synchronized。
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d",value);
testMethod(value - 1);
}
}
};
testMethod(10);
});
}
将NSRecursiveLock替换成了@synchronized之后,就没啥问题了,程序执行正常,结果也令人满意。
那么,为什么将NSRecursiveLock替换成了@synchronized之后就可以了呢?这就要对比一下NSRecursiveLock和@synchronized了。
虽然NSRecursiveLock和@synchronized都是递归锁,但是@synchronized当其后面的对象标识相同的时候,它就不会在没有解锁的情况下再次进入进行加锁,因此会从根本上杜绝死锁的情况发生。
根据上面?对于@synchronized、NSLock和NSRecursiveLock的分析,我们总结了一些小观点:
NSCondition
详见多线程——锁?
NSConditionLock
先来看下其声明:
@interface NSConditionLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
NSConditionLock是一把锁,一旦一个线程获得了锁,其他的线程就必须要等待。
[conditionLock lock]:表示期待获得锁,如果没有其他线程获得该锁(不需要判断其内部的condition),那么将执行它下面的代码;如果已经有其他的线程获得了该锁(可能是条件锁,也可能是无条件锁),那么就会等待,直到其他线程解锁。
[conditionLock lockWhenCondition:1]:如果没有其他的线程获得该锁,但是该锁内部的condition不等于1,此时依然不能获得该锁,依然需要等待;如果没有其他的线程获得该锁,并且该锁内部的condition等于1,则会进入代码区,同时设置它获得该锁,其他的线程将等待它代码的完成,直到他解锁。
[conditionLock unlockWithCondition:1]:表示释放锁,同时把内部condition设置为1条件。
[conditionLock lockWhenCondition:1 beforeDate:limitDate]:表示如果被锁定(注意,并没有获得该锁!),则超过时间limitDate后将不再阻塞该线程。
下面来看个例子:
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[conditionLock lockWhenCondition:1];
NSLog(@"线程 1");
[conditionLock unlockWithCondition:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
[conditionLock lockWhenCondition:2];
NSLog(@"线程 2");
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
执行后打印结果为:
2021-03-24 17:23:12.962065+0800 004-NSCondition[8242:1558008] 线程 3
2021-03-24 17:23:12.977179+0800 004-NSCondition[8242:1558008] 线程 2
2021-03-24 17:23:13.040493+0800 004-NSCondition[8242:1558006] 线程 1
分析如下:
自旋锁
最基本的锁,实际上就只有三类:自旋锁、互斥锁和读写锁。其他的比如条件锁、递归锁、信号量等都是基于三类基本锁在上层的一种封装。
前面我们讲了互斥锁,NSLock是最基本的互斥锁。互斥锁又分为递归锁和非递归锁,NSRecursive、@synchronized都是递归锁。
接下来我们就来聊聊自旋锁。
自旋锁(spinlock)是指当一个线程在获取自旋锁的时候,如果该自旋锁已经被其他的线程获取,那么当前线程将会循环等待,并且会持续不断地判断锁是否能够被成功获取,直到成功获得了锁之后才会退出循环。
等待获取锁的线程会一直处于活跃状态,虽然它并没有执行任何有效的任务,因此使用自旋锁会造成忙等待(busy-waiting)。
自旋锁能够避免进程上线文之间的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
我们平常在开发中接触到的最多的自旋锁就是atomic。关于atomic,我之前写过一篇文章atomic和nonatomic。
我们知道,atomic和nonatomic是声明属性的时候的原子修饰符,它们控制了在编译器自动为属性生成的setter和getter中是否进行加锁控制。
我接下来想看下其源码,因此我就到setter/getter的源码中去查看atomic和nonatomic的源码。
在libobjc源码中搜索“objc_setProperty”:就可以找到对应的setter源码:
它们都调用了reallySetProperty函数:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
使用到atomic的地方:
我们发现,如果不是atomic,那么就会直接修改值;如果是atomic,那么就加一把spinlock(自旋锁),然后才修改值。
同样的道理,我们看一下getter的源码:
如果不是atomic,那么就会直接返回值;如果是atomic,那么就加一把spinlock(自旋锁),然后才返回值。
所以说,atomic就是给系统自动生成的setter/getter方法内部自动加锁。
atomic虽然保证了对象的setter/getter方法的线程安全,但是并不能保证整个对象是线程安全的。
比如下面这个例子:
@interface ViewController ()
@property (atomic, strong) NSArray *array;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self lv_test_atomic2];
}
- (void)lv_test_atomic2{
//Thread A
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.array = @[@"Norman", @"Lavie", @"Lily"];
}
else {
self.array = @[@"Gaoying"];
}
NSLog(@"Thread A: %@\n", self.array);
}
});
//Thread B
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (self.array.count >= 2) {
NSString* str = [self.array objectAtIndex:1];
}
NSLog(@"Thread B: %@\n",self.array);
}
});
}
@end
A线程里面会不停的变换self.array里面的元素,有时是一个有时是三个。
B线程里面会在self.array里面的元素个数大于等于两个的时候获取第2个元素。
这样貌似没啥问题,但是运行之后你会发现,崩了:
2021-03-25 11:39:34.254859+0800 005-atomic分析[9944:1946565] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
数组越界了!为啥呢?我明明在获取之前判断数组的元素个数了啊。
这就是多线程不安全导致的。线程B中判断完了之后,线程A中将self.array里面的元素替换了,此时再获取[self.array objectAtIndex:1],自然就崩掉了。
该例也进一步验证了,atomic并不能保证对象是绝对线程安全的。
接下来再来看个例子:
@interface ViewController ()
@property (atomic, assign) NSInteger num;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self lv_test_atomic1];
}
- (void)lv_test_atomic1 {
//Thread A
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i ++) {
self.num += 1;
NSLog(@"%ld", (long)self.num);
}
});
//Thread B
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i ++) {
self.num += 1;
NSLog(@"%ld", (long)self.num);
}
});
}
@end
打印结果如下:
2021-03-25 11:50:49.352481+0800 005-atomic分析[10010:1967719] 1
2021-03-25 11:50:49.352522+0800 005-atomic分析[10010:1967717] 2
2021-03-25 11:50:49.353013+0800 005-atomic分析[10010:1967719] 3
2021-03-25 11:50:49.353015+0800 005-atomic分析[10010:1967717] 4
2021-03-25 11:50:49.353361+0800 005-atomic分析[10010:1967719] 5
2021-03-25 11:50:49.353444+0800 005-atomic分析[10010:1967717] 6
2021-03-25 11:50:49.353616+0800 005-atomic分析[10010:1967717] 7
......
2021-03-25 11:50:57.806527+0800 005-atomic分析[10010:1967719] 19920
2021-03-25 11:50:57.806857+0800 005-atomic分析[10010:1967717] 19921
2021-03-25 11:50:57.807141+0800 005-atomic分析[10010:1967719] 19922
2021-03-25 11:50:57.807497+0800 005-atomic分析[10010:1967717] 19923
2021-03-25 11:50:57.807818+0800 005-atomic分析[10010:1967719] 19924
2021-03-25 11:50:57.808150+0800 005-atomic分析[10010:1967717] 19925
2021-03-25 11:50:57.808510+0800 005-atomic分析[10010:1967719] 19926
2021-03-25 11:50:57.808799+0800 005-atomic分析[10010:1967717] 19927
2021-03-25 11:50:57.809163+0800 005-atomic分析[10010:1967719] 19928
2021-03-25 11:50:57.809406+0800 005-atomic分析[10010:1967717] 19929
2021-03-25 11:50:57.809727+0800 005-atomic分析[10010:1967719] 19930
2021-03-25 11:50:57.810282+0800 005-atomic分析[10010:1967719] 19931
2021-03-25 11:50:57.810548+0800 005-atomic分析[10010:1967719] 19932
2021-03-25 11:50:57.810913+0800 005-atomic分析[10010:1967719] 19933
2021-03-25 11:50:57.811225+0800 005-atomic分析[10010:1967719] 19934
2021-03-25 11:50:57.811585+0800 005-atomic分析[10010:1967719] 19935
2021-03-25 11:50:57.811849+0800 005-atomic分析[10010:1967719] 19936
按道理,两个线程一共执行了20000次,所以最后的结果应该是20000才对啊。为啥这里最后才到19936呢?
原因还是出在多线程上面。
self.num += 1实际上就是self.num = self.num + 1,这里包含了属性的getter和setter,它们都是atomic的,因此其内部都是线程安全的。现在有可能会出现这样一种情形:
线程A中的self.num的getter完了之后,线程B中的getter获取到锁了,然后在线程B中的getter完成之后,线程A中的setter和线程B中的setter在赋值的时候都是获取到的同一个值大小。按道理的话,两次setter应该加2,但是这种情形下这两次setter只加了1。
所以,就会出现最终的打印结果没有到20000的情况。
所以,atomic只是保证了getter/setter存取方法的线程安全,并不能保证整个对象是线程安全的,也就是说,是否使用atomic跟属性的多线程安全并没有什么直接的联系,在编程的时候,线程安全还是需要开发者自己来处理。
而在效率层面,atomic需要锁住资源,因此比nonatomic慢了20倍,所以综合考虑,我们在iOS开发过程中要尽量使用nonatomic。
读写锁
读写锁是一种特殊的自旋锁,它把共享资源的访问者划分为读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
读写锁相对于自旋锁而言,能够提高并发性。因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读写数为实际的逻辑CPU数。
写者是具有排他性的,一个读写锁同时只能有一个写者或者多个读者,但是不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。
当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。
通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求, 这样可以避免读模式锁⻓期占用, 而等待的写模式锁请求⻓期阻塞。
读写锁适合于对数据结构的读次数比写次数多得多的情况。因为读模式锁定时可以共享,以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。
总结
本文介绍了iOS开发过程中的各个锁,有些是着重讲的,有些一带而过。下面附带各个锁的性能对比,诸位在开发过程中可以根据具体业务场景,再结合性能,去选择最合适的锁。
以上。