iOS 中的锁
本文主要通过Objective-C语言进行体现,其实跟Swift也差不多。
1. 基本概念
锁的存在主要就是解决资源抢夺的问题,在iOS中的锁基本分为两种,分别是互斥锁和自旋锁,其实读写锁也可以算一种,但是读写锁也是一种特殊的自旋锁。另外对于条件锁、递归锁、信号量基本都是上层的封装实现。
1.1 互斥锁
自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很 短时间的场合是有效的。
- 互斥锁:顾名思义,就是相互排斥,是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成,也就是说一个线程获得锁后,其他线程在其释放锁之前都获取不到锁。互斥锁也分为两种分别是递归锁和非递归锁。
- 递归锁:对于互斥锁我们可以使用递归的方式进行锁定,简答的说就是可以重新进行锁定,在同一个线程释放锁前可以再次获取锁进行锁定,并且不会造成死锁。
- 非递归锁:跟递归锁相反,不可以重新被锁定,必须等锁释放后才能再次获得锁
1.2 自旋锁
- 自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行, 因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放。简单来说就是线程A获取到锁,在其释放锁之前,线程B又来获取锁,此时获取不到,线程B就会不断的进入循环,一直检查锁是否已被释放,如果释放,则能获取到锁。
1.3 互斥锁和自旋锁的区别
- 互斥锁:当线程获取锁却没有获取到时线程会进入休眠状态,等锁被释放时,线程会被唤醒,同时获取到锁,继续执行任务,互斥锁会改变线程的状态。
- 自旋锁:当线程获取锁但没获取到时,不会进入休眠,而是一直循环等待,线程始终处于活跃状态,不会改变线程的状态。
1.4 使用场景
- 互斥锁:由于其会改变线程的状态,这就需要内核不断的调度线程资源,因此效率上比自旋锁要低一些。理论上来说其实不适合使用自旋锁的地方都可以使用互斥锁。
- 自旋锁:在等待锁期间线程是活跃度,所以这种活跃在一定时间内是个死循环,会消耗更多的
CPU资源,自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。由于自旋锁的线程活跃也就使得它在递归调用的时候会产生死锁。
1.4 死锁
死锁就是字面意思,锁上了解不开,不解锁就不能继续执行,基本就是两个线程的相互等待,最后谁也等不到,这里说明一下阻塞和死锁的理解误区,阻塞就是不能继续执行了是线程内的等待,死锁是线程间的等待,本质上是不一样的。
1.5 其他锁
- 条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
- 信号量:其实信号量算不上锁,它只是一种更高级的同步机制,在互斥锁中
semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间用来实现更加复杂的同步,而不单单是线程间互斥。
2. NSLock
NSLock在分类中属于互斥锁,是我们在使用Objective-C进行开发时常用的一种锁。看了好多文章说NSLock是非递归锁,确实NSLock的递归上会引起阻塞或者崩溃,但是在同一线程内NSLock也可以再次加锁,所以在这一点也不绝对。
2.1 NSLock 定义
我们点击跳转到NSLock的定义处,源码如下:
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}bv
2.2 NSLocking 协议
从上一节中我们可以看到NSLock遵守一个NSLocking的协议,协议定义如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
我们可以看到协议中的lock和unlock方法就是我们常用的加锁解锁的方法。值得注意的是:-lock和-unlock必须在相同的线程中成对调用,否则就会产生未知的结果。
2.3 NSLock的其他方法
对于NSLock还有另外两个方法和一个属性,定义在源码的下面,代码如下:
// 尝试获取锁,获取到返回YES,获取不到返回NO
- (BOOL)tryLock;
// 在指定时间前获取锁,能够获取到返回YES,获取不到返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 锁名称,如果使用锁出现异常,输出的log中会有锁的名称打印
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
2.4 NSLock 使用示例
这里我们模拟一个售票系统,如果不加锁的话就会导致一张票被卖多次的情况;加锁后才能保证票数的准确。
2.4.1 基本用法示例
- (void)testNSLock3 {
self.lock = [[NSLock alloc] init];
NSThread *thread1 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
thread1.name = @"1号窗口";
[thread1 start];
NSThread *therad2 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
therad2.name = @"2号窗口" ;
// therad2.threadPriority = 0.8;
[therad2 start];
NSThread *therad3 = [[NSThread alloc]initWithTarget:self selector:@selector(therad:) object:nil];
therad3.name = @"3号窗口" ;
// therad3.threadPriority = 1 ;
[therad3 start];
}
//模拟售票
-(void)therad:(id)object{
//票数100张
static int number = 100 ;
while (1) {
// 线程加锁,提高数据访问的安全性
[self.lock lock];
number--;
NSLog(@"%@ %d",[[NSThread currentThread]name],number);
//模拟等待
// sleep(1);
if (number == 0) { break ; }
[self.lock unlock] ;
}
}
2.4.2 tryLock
- (void)testNSLock5 {
//主线程中
NSLock *lock = [[NSLock alloc] init];
//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock];
NSLog(@"线程1");
sleep(10);
NSLog(@"睡醒了");
[lock unlock];
});
//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);//以保证让线程2的代码后执行
if ([lock tryLock]) {
NSLog(@"线程2");
[lock unlock];
} else {
NSLog(@"尝试加锁失败");
}
});
}
打印结果:
根据打印结果我们可以看到,tryLock返回NO后也不会阻塞线程,还继续执行下面的代码。
2.4.3 lockBeforeDate
如果将2.4.2中的tryLock换成[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]就会阻塞线程,它将在Date前尝试加锁,如果在指定时间前都不能加锁则返回NO,加锁失败后跟上面打印是一致的。如果加锁成功打印结果如下:
打印结果:
2.4.4 死锁
- (void)viewDidLoad {
[super viewDidLoad];
self.lock = [[NSLock alloc] init];
// [NSThread detachNewThreadSelector:@selector(testLock1) toTarget:self withObject:nil];
[self testLock1];
}
- (void)testLock1 {
[self.lock lock];
NSLog(@"testLock1: lock");
[self testLock2];
[self.lock unlock];
NSLog(@"testLock1: unlock");
}
- (void)testLock2 {
[self.lock lock];
NSLog(@"testLock2: lock");
[self.lock unlock];
NSLog(@"testLock2: unlock");
}
这里只会打印testLock1: lock,在同一线程内如果没有解锁就再次加锁的话就会造成死锁。这里就是testLock2等待testLock1解锁,而testLock1也在等testLock2解锁。
2.5 NSLock 底层实现
通过上面的NSLock定义我们可以知道NSLock是在Foundation库中实现的,但是Foundation的开源代码只在Swift中有,本着有就比没有强的思想我们下载一个Swift CoreLibs Foundation源码一探究竟。
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是对pthread互斥锁(mutex)的封装,我们可以看到在lockBbeforeLimit方法中会调用timedLock这个方法,这也是在Date前实现加锁的真正实现,我们跳转到该方法(PS:在源码中有Windows平台的实现,这里我们就不看了,直接看else的部分)进行查看:
timedLock 源码:
private func timedLock(mutex: _MutexPointer, endTime: Date,
using timeoutCond: _ConditionVariablePointer,
with timeoutMutex: _MutexPointer) -> Bool {
var timeSpec = timeSpecFrom(date: endTime)
while var ts = timeSpec {
let lockval = pthread_mutex_lock(timeoutMutex)
precondition(lockval == 0)
let waitval = pthread_cond_timedwait(timeoutCond, timeoutMutex, &ts)
precondition(waitval == 0 || waitval == ETIMEDOUT)
let unlockval = pthread_mutex_unlock(timeoutMutex)
precondition(unlockval == 0)
if waitval == ETIMEDOUT {
return false
}
let tryval = pthread_mutex_trylock(mutex)
precondition(tryval == 0 || tryval == EBUSY)
if tryval == 0 { // The lock was obtained.
return true
}
// pthread_cond_timedwait didn't timeout so wait some more.
timeSpec = timeSpecFrom(date: endTime)
}
return false
}
- 首先设定超时时间
- 然后开启
while循环 - 在循环内通过
pthread_cond_timedwait函数进行计时等待,线程进入休眠 - 如果超时直接返回
false - 如果等待没超时,并在这期间锁被释放,则线程被唤醒,再次通过
pthread_mutex_trylock函数尝试获取锁 - 如果获取成功则返回
true - 如果没有超时,但是别唤醒后也没有获取到锁(被其他线程抢先获得),则重新计算超时时间进入下一次
while循环
2.6 小结
至此我们对NSLock的分析就完毕了,总结如下:
- 在使用
NSLock的时候lock和unlock方法的使用时成对出现的 - 切记不要在同一线程中连续加锁又不解锁,防止死锁的出现
tryLock方法不会阻塞线程lockBeforeDate方法会在超时前阻塞线程
3. @synchronized
@synchronized是我们在使用Objective-C开发时使用最多的一把锁了由于代码简单且方便实用深得广大开发者喜欢。但是很多人并不知道@synchronized底层实现是个递归锁,不会产生死锁,且不需要程序猿手动去加锁解锁。下面我们就慢慢揭开@synchronized的面纱。
3.1 @synchronized 实现探索
由于@synchronized是关键字,我们并不能直接查看它的具体实现。此时我们编写如下代码:并添加断点,通过汇编进行初步探索
要想查看汇编则需要在Xcode->Debug->Debug Workflow->Always Show Disassembly选中。
由上面的汇编代码我们可以看到在NSLog的上下分别有objc_sync_enter和objc_sync_exit的调用(bl)。其实这里objc_sync_enter就是加锁,objc_sync_exit就是解锁。一般objc开头的方法,基本都是在objc源码中,下面我们打开objc源码一探究竟。此处使用的是objc4-779.1。
3.2 objc_sync_enter 探索
3.2.1 objc_sync_enter源码分析
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;
}
通过注释我们可以看出该函数:
- 首先在
objc上同步 - 如果需要就分配一个与
objc相关联的递归锁 - 一旦获得锁就返回
OBJC_SYNC_SUCCESS
其实代码上也跟上面说的一致,只是还有些细微的处理:
- 如果锁定的对象是空的,就不进行任何加锁操作
- 这里的
result一直是OBJC_SYNC_SUCCESS,所以说就算因为对象obj为空导致加锁失败也不会阻塞线程,而是直接向下执行。 - 如果
obj不为空,那么回调用id2data函数获取一个SyncData类型的对象 - 这里如果获取不到锁会通过
data->mutex.lock();阻塞当前线程,等其他线程释放锁后继续向下执行。
3.2.2 关于加锁(id2data)进一步探索
id2data有两个参数,第一个是锁定对象obj,第二个ACQUIRE这里是个枚举值ACQUIRE的意思是加锁,其实还有两个分别是RELEASE意思是解锁和CHECK检查锁的状态。
我们在来看看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;
通过SyncData源码我们可以看到SyncData是一个结构体,拥有4个成员:
nextData:指向下一个节点,像极了一个链表的节点object:一个OC对象,其实它就是保存被锁对象的obj的。threadCount:记录线程数,其实就是调用synchronized代码块的线程数mutex:递归锁,这就底层实际的锁,通过调用它的lock()方法实现加锁操作。
id2data函数分析
由于id2data代码比较多,这里我们通过折叠代码先来简单看看
- 首先第一块就是初始化一个局部的锁,以及一个
SyncData指针,用作在链表中查找用也就是链表的头指针,还有一个result用作存储返回结果 - 第而部分就是在单线程的缓存中进行查找了(查找是为了查找加锁对象)
- 下一步分就是在单条线程上没有查找到则需要进行全局缓存的查找
- 接下来就是都没有查找到,就说明是第一次给该
OC对象加锁,所以要进行第一次存储 - 最后就是对查找对象的处理了
下面我们来一步一步的分析,首先看看前三行代码
第一行就是获取一个锁,这是个局部变量,在本函数内需要使用的锁,看名字spinlock_t是个自旋锁,那么我们来看看它的实现的,源码如下:
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
// 此处省略80多行代码...
}
os_unfair_lock
/*!
* @typedef os_unfair_lock
*
* @abstract
* Low-level lock that allows waiters to block efficiently on contention.
*
* In general, higher level synchronization primitives such as those provided by
* the pthread or dispatch subsystems should be preferred.
*
* The values stored in the lock should be considered opaque and implementation
* defined, they contain thread ownership information that the system may use
* to attempt to resolve priority inversions.
*
* This lock must be unlocked from the same thread that locked it, attempts to
* unlock from a different thread will cause an assertion aborting the process.
*
* This lock must not be accessed from multiple processes or threads via shared
* or multiply-mapped memory, the lock implementation relies on the address of
* the lock value and owning process.
*
* Must be initialized with OS_UNFAIR_LOCK_INIT
*
* @discussion
* Replacement for the deprecated OSSpinLock. Does not spin on contention but
* waits in the kernel to be woken up by an unlock.
*
* As with OSSpinLock there is no attempt at fairness or lock ordering, e.g. an
* unlocker can potentially immediately reacquire the lock before a woken up
* waiter gets an opportunity to attempt to acquire the lock. This may be
* advantageous for performance reasons, but also makes starvation of waiters a
* possibility.
*/
OS_UNFAIR_LOCK_AVAILABILITY
typedef struct os_unfair_lock_s {
uint32_t _os_unfair_lock_opaque;
} os_unfair_lock, *os_unfair_lock_t;
根据上面的注释,也就是下面这句,os_unfair_lock是用来替代OSSpinLock这个自旋锁的互斥锁,不会自旋,在内核中等待被唤醒。所以说spinlock_t并不是如它的名字一般,而是个互斥锁。
Replacement for the deprecated OSSpinLock. Does not spin on contention but waits in the kernel to be woken up by an unlock.
第二行代码获取了一个SyncData类型的二重指针,我们通过查看SyncData的定义知道它是一个链表结构,所以说这个listp就是链表的头指针。对于宏LIST_FOR_OBJ代码如下:
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
可以看到这个宏是个全局静态变量sDataLists,以obj为索引获取值,获取到的对象类型为StripedMap<SyncList>,同时对其取data取地址进行返回。对于StripedMap是一个类,如下图:
- 通过图中的第834行代码我们可以看到,
p就是我们传进来的对象obj,从数组中取值返回 - 数组的索引通过
indexForPointer这个方法计算得出。 - *算法是取出对象地址,然后对地址右移四位 异或 上地址右移9的结果,然后对
StripeCount取余,这StripeCount在我们的iPhone上只有8位,在类的一开始就定义了。 - 这个
8就是iPhone真机上所能使用哈希表的大小 - 对于哈希表就可能存在哈希冲突,但是在这里是通过这个方法去取链表,这个哈希表的数据是某条链表的头结点,并且每条链表中还会有很多个节点,每个节点又保存了和不同对象相关联的锁。
第三行代码就是定义了一个result值为NULL,是id2data()需要返回的结果,就没啥说的了。
接下来是在单线程中查找,代码如下:
- 首先我们看到的是一个宏定义
SUPPORT_DIRECT_THREAD_KEYS它的值是1,并且在#if defined(__PTK_FRAMEWORK_OBJC_KEY0)下才会被定义为1,虽然具体什么意思不太明了,但是大概能看出来是在objc的某个环境下使用,也正是我们需要研究的 - 首先看注释就能很好的理解该分支是在单线程中查找缓存
fastCacheOccupied标记当前线程的快速缓存是否已被占用- 通过
SYNC_DATA_DIRECT_KEY和tls_get_direct函数从线程的快速缓存中取出一个SyncData节点,其实单线程私有数据只保存一个节点的地址。 - 如果这个节点没有值也就拜拜了,直接跳过,如果有值就标记
fastCacheOccupied为YES,即使这个节点中没有我们要找的被锁对象 - 如果快速缓存中恰好是当前对象关联的锁,那么对这个锁的计数
+1,如果是解锁就-1(这些计数是当前线程的私有数据,其他线程访问不到) - 如果找到,则会返回,如果找不到就会到下一流程进行处理了
在单线程缓存中查找不到后,就会来到下面的全局缓存中进行查找
- 首先通过
fetch_cache函数获取整体的缓存对象SyncCache
SyncCache 和 SyncCacheItem 结构体:
两个结构体实现如下,详见注释,对于SyncCacheItem就一看就明了了。
typedef struct SyncCache {
unsigned int allocated; // 保存`SyncCacheItem`的总数
unsigned int used; // 保存使用的数量
SyncCacheItem list[0]; // 缓存链表头结点地址
} SyncCache;
typedef struct {
SyncData *data;
unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;
fetch_cache 源码:
static SyncCache *fetch_cache(bool create)
{
_objc_pthread_data *data;
data = _objc_fetch_pthread_data(create);
if (!data) return NULL;
if (!data->syncCache) {
if (!create) {
return NULL;
} else {
int count = 4;
data->syncCache = (SyncCache *)
calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
data->syncCache->allocated = count;
}
}
// Make sure there's at least one open slot in the list.
if (data->syncCache->allocated == data->syncCache->used) {
data->syncCache->allocated *= 2;
data->syncCache = (SyncCache *)
realloc(data->syncCache, sizeof(SyncCache)
+ data->syncCache->allocated * sizeof(SyncCacheItem));
}
return data->syncCache;
}
- 这里就是返回
SyncCache数据,_objc_pthread_data也是一个结构体 - 其实在这个方法中就是通过
_objc_fetch_pthread_data去获取这个data,(函数内还调用了好几步)这里就不一层一层的去分析了,感兴趣的可以自己点击跳转就跟一下 - 这个方法其实重要的还是做了个扩容,初始值是4,如果不够了就进行2倍扩容
如果来到下面这段代码就说明我们在任何缓存中都没有找到当前对象的锁,说明是第一次给这个对象加锁。
- 首先就是使用一开始定义的局部锁变量进行加锁
- 然后通过局部变量
p遍历循环链表 - 这里在遍历过程中如果找到了要加锁的对象,但是能到这里说明这个对象不被任何线程占用,所以直接使用这个对象就可以了,不需要重新开辟内存空间插入链表
- 如果没有的话就通过变量
firstUnused记录第一个没有使用的节点 - 如果不是加锁到这里就可以
goto done 了 - 最后如果
firstUnused不为空对result进行一系列赋值操作后就可以goto done 了 - 过了这个代码块,说明是第一次加锁使用缓存哈希表,则创建个新的节点放在表头(也有可能是某一条链表中节点都被使用,重新开启一条链表,也就是上面提到的8条的哈希表中的一个)。
最后我们goto done
在done这个模块主要是对result的一些处理
- 首先是解锁
- 然后判断
result如果是空的话就直接调用下面的返回了,返回了一个NULL - 如果是解锁的话就直接返回
nil,如果也不是加锁就报错了,如果加锁对象不匹配也会报错,其实这块就是容错处理,基本不会来到 - 下面就是对快速缓存的处理,如果线程的快速缓存没有被占用就存储到快速缓存中,如果被占用就将节点存储到全局缓存中
关于快速缓存:前面分析的时候无论在线程缓存中是否找到被锁的对象(前提是线程快速缓存存在)fastCacheOccupied都会被置为YES,也就是说线程私有数据的快速缓存只缓存一次,且只保存第一次的这一个节点指针。我觉得就是你锁了一次,下次在锁的概率很大,使用频率也会超级高,因为在锁定一个对象的时候大多情况都是多线程操作这个对象,在短时间内操作频率足够高,如果不高的话可能也不至于用锁,还有可能该对象的同步锁已经被其他线程缓存到其他线程的私有数据了,当前线程又无法访问其他线程的私有数据,如果替换的话,会重复缓存。
3.2.3 objc_sync_exit
关于解锁我们也是直接看源码了,代码如下:
// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
解锁的代码就很简单了,几步判断,核心步骤也是通过id2data进行处理的,在加锁代码分析的时候多提到过。
- 首先定义一个局部的
result - 判断
obj是否有值,没值就是对空对象解锁直接返回就好了 - 如果有值就经过
id2data返回要解锁的节点 - 如果没返回节点就给
result赋值error然后返回 - 如果返回值了就调用
tryUnlock去解锁 - 如果解锁失败也是给
result赋值error然后返回 - 其他情况返回一开始定义的
result的默认值就好了
3.3 @synchronized 总结
3.3.1 注意事项
@synchronized不能锁定和解锁空对象,所以在使用的时候一定要注意锁定对象不能是空值否则就会出现加锁失败的情况,达不到我们预期的效果(空对象也不会影响代码的执行)@synchronized对于指针不断变化的属性或者成员变量可能不符合我们想要通过锁来解决问题的初衷,所以在使用它的时候要特别注意对加锁对象生命手气和指针指向的变化
3.3.2 @synchronized
@synchronized属于递归锁,在同一线程内可重新加锁,在其内部有个持有锁的计数器@synchronized加锁是调用的int objc_sync_enter(id obj)函数@synchronized解锁是调用的int objc_sync_exit(id obj)函数- 在上面的两个函数中都主要通过
id2data这个函数来存储和获取SyncData锁对象-
在
id2data中首先在线程的快速缓存中查找锁对象节点 -
如果找不到就去全局缓存中查找
-
如果全局缓存中没有就说明是第一次锁定该对象,会从全局缓存的链表中遍历找到第一个空闲节点存储该对象关联的新节点
-
如果没找到空节点就重新开辟一个新的链表,将该对象关联的节点存储为链表的表头
-
最后将节点的结果存储到线程的私有数据中,并保存早全局缓存中
-
对于解锁跟以上步骤一致,只是通过传入的加解锁标志
why(枚举类型)进行不同的处理,最后返回待解锁节点,通过调用mutex.tryUnlock();进行解锁 -
在没条线程中维护这锁的计数,没加锁一次计数加一,解锁则减一
-
关于全局缓存,实际上是一张哈希表,通过对锁对象
obj的指针地址进行哈希计算得出索引 -
hash表的的Value是链表第一个节点(SyncData)的地址,每个链表中每个节点对应不同的锁对象(SyncData)
4. NSCondition
-
NSCondition也是一把互斥锁,有条件的互斥锁,跟NSLock定义在同一个文件中(点击跳转就可以看见),下面我们看一看NSCondition的源码。
4.1 NSCondition 定义
API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0))
@interface NSCondition : NSObject <NSLocking> {
@private
void *_priv;
}
- (void)wait;
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;
- (void)broadcast;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
我们可以看到NSCondition也是遵守NSLocking协议的,除了这些,与NSLock同样有一个*_priv;的私有成员变量和name属性。同时还有四个方法:
wait:阻塞当前线程,使线程进入休眠,等待唤醒信号,如果直接在主线程调用wait就GG了。waitUntilDate::该方法与wait基本一致,只不过是多了一个超时时间,如果在这个时间前都不能被唤醒就不再阻塞当前线程。signal:唤醒一个正在等待的线程,如需唤醒多个线程则需要调用多次,如果没有线程在等待,则什么也不做。broadcast:唤醒所有线程,如果没有线程在等待,则什么也不做。
4.2 NSCondition 源码探索
我们同样来到Swift CoreLibs Foundation源码一探究竟。
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
internal var cond = _ConditionVariablePointer.allocate(capacity: 1)
public override init() {
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(cond)
#else
pthread_mutex_init(mutex, nil)
pthread_cond_init(cond, nil)
#endif
}
deinit {
#if os(Windows)
// SRWLock do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
pthread_cond_destroy(cond)
#endif
mutex.deinitialize(count: 1)
cond.deinitialize(count: 1)
mutex.deallocate()
cond.deallocate()
}
open func lock() {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
#else
pthread_mutex_unlock(mutex)
#endif
}
open func wait() {
#if os(Windows)
SleepConditionVariableSRW(cond, mutex, WinSDK.INFINITE, 0)
#else
pthread_cond_wait(cond, mutex)
#endif
}
open func wait(until limit: Date) -> Bool {
#if os(Windows)
return SleepConditionVariableSRW(cond, mutex, timeoutFrom(date: limit), 0)
#else
guard var timeout = timeSpecFrom(date: limit) else {
return false
}
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
#endif
}
open func signal() {
#if os(Windows)
WakeConditionVariable(cond)
#else
pthread_cond_signal(cond)
#endif
}
open func broadcast() {
#if os(Windows)
WakeAllConditionVariable(cond)
#else
pthread_cond_broadcast(cond)
#endif
}
open var name: String?
}
其实一看源码就能够知道NSCondition是对pthread_mutex_t的封装,是个有条件(Condition)的NSLock,因为它使用了pthread_cond_t的条件,NSLock是自动管理线程的休眠与唤醒的,NSCondition可以让程序员手动管理线程的休眠与唤醒,这样的特性可以更灵活的控制线程的状态。
4.3 NSCondition 使用示例
这里采用一个生产者消费者的案例来体现NSCondition的作用,在为生产的时候是不能消费的,示例代码如下:
- (void)testConditon{
_testCondition = [[NSCondition alloc] init];
//创建生产-消费者
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self lg_consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self producer];
});
}
}
- (void)producer{
[_testCondition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd--- 线程%@",self.ticketCount, [NSThread currentThread]);
// sleep(1);
[_testCondition signal];
[_testCondition unlock];
}
- (void)consumer{
// 线程安全
[_testCondition lock];
while (self.ticketCount == 0) {
NSLog(@"等待 count %zd--- 线程%@",self.ticketCount, [NSThread currentThread]);
// 保证正常流程
[_testCondition wait];
// [_testCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
// if ([_testCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]]) {
// NSLog(@"被信号唤醒");
// } else {
// NSLog(@"已超时--- 线程%@", [NSThread currentThread]);
// }
// NSLog(@"执行完等待代码--- 线程%@", [NSThread currentThread]);
}
// NSLog(@"不等了");
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd --- 线程%@",self.ticketCount, [NSThread currentThread]);
[_testCondition unlock];
}
在以上示例代码中,我们对消费线程进行了等待处理,这在日常应用中也是非常常见的,如果没有生产出来是无法被消费的。对于生产线程,我们这里没有过多要求,如果我们有生产上限,比如生产到一定数量就不在继续生产了,下面我们在通过修改该示例对上限也作出限制,代码如下:
- (void)producer2{
[_testCondition lock];
if (self.ticketCount >= producerMax) {
[_testCondition wait];
// [_testCondition waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd--- 线程%@",self.ticketCount, [NSThread currentThread]);
// 只有数量为1的时候在发送解锁信号,因为数量大于1并不需要等待
if (self.ticketCount == 1) {
[_testCondition signal];
}
[_testCondition unlock];
}
- (void)consumer2{
// 模拟消费等待
sleep(3);
// 线程安全
[_testCondition lock];
while (self.ticketCount == 0) {
NSLog(@"等待 count %zd--- 线程%@",self.ticketCount, [NSThread currentThread]);
// 保证正常流程
[_testCondition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
NSLog(@"消费一个 还剩 count %zd --- 线程%@",self.ticketCount, [NSThread currentThread]);
// 当数量小于生产上限后发送解锁信号,让生产线程继续生产
if (self.ticketCount < producerMax) {
[_testCondition signal];
}
[_testCondition unlock];
}
4.4 小结
- 关于
NSCondition底层调用就是pthread_mutex_lock - 在调用
wait和waitUntilDate:方法后一定要在合适的位置进行唤醒,否则就会一直等待,造成线程阻塞 - 本质上与
NSLock没啥区别,只是上层的一种封装而已,给程序员提供更多的自我操作
5. NSConditionLock
NSConditionLock是一把条件锁,也是互斥锁的一种,光看名字就能够知道它与NSCondition有着不可分割的关系,可以说NSConditionLock就是NSCondition的一种延伸,将条件这个抽象的概念的定义交给程序员。
Using an NSConditionLock object, you can ensure that a thread can acquire a lock only if a certain condition is met. Once it has acquired the lock and executed the critical section of code, the thread can relinquish the lock and set the associated condition to something new. The conditions themselves are arbitrary: you define them as needed for your application. 译文:使用
NSConditionLock对象,可以确保线程只有在满足特定条件时才能获得锁。一旦它获得了锁并执行了代码的关键部分,线程就可以放弃锁并将相关条件设置为新的条件。条件本身是任意的:您可以根据应用程序的需要定义它们。
5.1 NSConditionLock 定义
我们直接点击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的定义源码我们可以知道,NSConditionLock除了同样遵守NSLocking协议和有一个私有的*_priv成员变量和name属性外还有下列方法和属性:
- 首先是有一个
initWithCondition:快速初始化方法 - 其次就是一个
NSInteger类型的condition只读属性,这就是抽象的条件,类似于tag,一个标签就代表一个条件。另外它是只读的,不可以直接修改属性。 lockWhenCondition:获取指定条件的锁,如果与属性condition相等则可以获取到锁,否则阻塞线程进行等待。tryLock:尝试获取锁,这里不会判断条件,当有锁释放后就可以进行获取锁,如果获取到锁就会返回YES,获取不到就会返回NO,获取不到也不会阻塞线程的执行。tryLockWhenCondition:尝试对某个条件去获取锁,如果获取不到就返回NO获取到就会返回YES。这里无论返回结果如何都不会阻塞线程,这是它与lockWhenCondition:的本质区别。unlockWithCondition:解锁并变更到另一个条件,就是释放锁,然后修改condition属性。lockBeforeDate:在指定时间前获取锁,能够获取到返回YES,获取不到返回NO。lockWhenCondition:beforeDate:在指定时间前获取指定条件的锁能够获取到返回YES,获取不到返回NO。
5.2 NSConditionLock 源码探索
我们同样来到Swift CoreLibs Foundation源码一探究竟。
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal var _value: Int
internal var _thread: _swift_CFThreadRef?
public convenience override init() {
self.init(condition: 0)
}
public init(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(before: Date.distantFuture)
}
open func unlock() {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_cond.broadcast()
_cond.unlock()
}
open var condition: Int {
return _value
}
open func lock(whenCondition condition: Int) {
let _ = lock(whenCondition: condition, before: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(before: Date.distantPast)
}
open func tryLock(whenCondition condition: Int) -> Bool {
return lock(whenCondition: condition, before: Date.distantPast)
}
open func unlock(withCondition condition: Int) {
_cond.lock()
#if os(Windows)
_thread = INVALID_HANDLE_VALUE
#else
_thread = nil
#endif
_value = condition
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread != nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
open func lock(whenCondition condition: Int, before limit: Date) -> Bool {
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
#if os(Windows)
_thread = GetCurrentThread()
#else
_thread = pthread_self()
#endif
_cond.unlock()
return true
}
open var name: String?
}
5.2.1 三个属性:
_cond一个NSCondition对象。_value记录当前条件值,也就是上面OC中对应的condition,后面也有体现return _value。_thread记录当前持有锁的线程。
5.2.2 加锁
关于加锁这里大概分为两部分,
第一部分:
lock、tryLock、lockBeforeDate都是调用的lockBeforeDate也是Swift源码中的lock(before limit:方法,这个方法通过判断线程是否为空进行进行阻塞等待,如果线程有值则调用NSCondition的wait(until limit:方法也就是OC对应的waitUntilDate:方法进行加锁处理。
第二部分:
lockWhenCondition:、tryLockWhenCondition:、lockWhenCondition:beforeDate:都是调用的lockWhenCondition:beforeDate:方法也就是Swift中对应的lock(whenCondition:before limit:方法,这个方法通过判断线程是否为空或者条件与_value的值是否相等进行阻塞等待,在条件满足后统样调用NSCondition的wait(until limit:方法也就是OC对应的waitUntilDate:方法进行加锁处理。
所以说NSConditionLock的加锁本质上就是调用的NSCondition的wait(until limit:方法也就是OC对应的waitUntilDate:方法,对于条件,只是NSConditionLock内部进行封装判断而已。
5.2.3 解锁
关于解锁这里就更简单了,这里将线程置空,然后调用NSCondition的broadcast方法唤醒所有等待锁的线程,对于unlock(withCondition condition:方法,在唤醒前,只是增加将当前条件更新的代码。
5.3 NSConditionLock 使用示例
5.3.1 基本用法
- (void)testConditonLock{
// NSConditionLock
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];
[conditionLock unlock];
});
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];
sleep(2);
NSLog(@"线程 3");
[conditionLock unlock];
});
}
以上代码的执行结果有很多,但有一点就是2肯定在1的前面,因为初始化条件是2,需要等待2执行完变换成1才能执行1,虽然线程1的队列优先级高,但也需要等待锁。最后3是没有条件的,它与其他线程谁先获得锁就先执行谁。
5.3.2 lockWhenCondition:beforeDate:
如果将5.3.1中线程1中的代码换成如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if ([conditionLock lockWhenCondition:1 beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]) {
NSLog(@"线程 1");
} else {
NSLog(@"获取锁失败");
}
[conditionLock unlock];
});
更改代码后就会先打印获取锁失败了,这里lockWhenCondition:beforeDate:方法在一定时间内获取不到锁就返回NO继续向下执行,不在阻塞线程了,如果将时间修改的多一些就会跟5.3.1的结果保持一致。(PS:这个时间修改成多一些只是跟本例子有关)
2.3.3 tryLockWhenCondition:
如果将5.3.2中的代码修改为如下代码:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if ([conditionLock tryLockWhenCondition:1]) {
NSLog(@"线程 1");
} else {
NSLog(@"获取锁失败");
}
[conditionLock unlock];
});
修改成这样后就会先打印获取锁失败,这里tryLockWhenCondition:无论返回结果如何都不会阻塞线程。
5.4 小结
NSConditionLock如果不加条件就跟NSLock一模一样NSConditionLock实际上是对NSCondition的封装,将条件更细粒度的交给程序员去判断- 由于
NSConditionLock同样遵守NSLocking协议,所以在调用lock和unlock方法时不会管当前condition是什么就会获取锁,或者解锁不改变condition的值。所以即使条件满足lock也有可能比lockWhenCondition提前获取锁。 lockWhenCondition会阻塞线程,如果一直没有满足条件的锁释放,则永远获取不到锁。NSConditionLock最大的用处就是指定多线程任务的执行顺序
6. NSRecursiveLock
NSRecursiveLock也是一把互斥锁,但是它是互斥锁中的递归锁,所谓递归在锁的第一篇文章中就已经提到了,在同一线程中可以再次获取锁去加锁,而不会造成死锁。简单来说就是加锁执行一段代码,但是代码中调用的另一段代码也要获取这个锁去加锁执行,这里就递归锁应用的地方。
6.1 NSRecursiveLock 定义
@interface NSRecursiveLock : NSObject <NSLocking> {
@private
void *_priv;
}
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@end
这里跟NSLock的定义差不多
- 同样遵守
NSLocking协议 *_priv私有成员变量和name属性。tryLock尝试获取锁,获取到返回YES,否则返回NO。不会阻塞线程。lockBeforeDate:尝试在指定时间前获取锁,获取到返回YES,否则返回NO,在指定时间前会阻塞线程。
6.2 NSRecursiveLock 源码探索
我们同样来到Swift CoreLibs Foundation源码一探究竟。
open class NSRecursiveLock: NSObject, NSLocking {
internal var mutex = _RecursiveMutexPointer.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() {
super.init()
#if os(Windows)
InitializeCriticalSection(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
#if CYGWIN
var attrib : pthread_mutexattr_t? = nil
#else
var attrib = pthread_mutexattr_t()
#endif
withUnsafeMutablePointer(to: &attrib) { attrs in
pthread_mutexattr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
deinit {
#if os(Windows)
DeleteCriticalSection(mutex)
#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)
EnterCriticalSection(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
LeaveCriticalSection(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 TryEnterCriticalSection(mutex)
#else
return pthread_mutex_trylock(mutex) == 0
#endif
}
open func lock(before limit: Date) -> Bool {
#if os(Windows)
if TryEnterCriticalSection(mutex) {
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?
}
其实乍一看NSRecursiveLock源码跟NSLock差不多,但是仔细一看就能发现在NSRecursiveLock中对于互斥锁mutex设置了递归属性,主要体现在如下两个地方:
其他的调用和方法实现跟NSLock几乎一致,可以参考我的这篇文章,这里只是对底层pthread_mutex使用了递归属性的锁,具体pthread_mutex内部是什么样的实现的以及互斥锁是否可以递归,可以试着去研究一下pthread或者POSIX。
6.3 NSRecursiveLock 使用示例
6.3.1 示例1
其实NSRecursiveLock最合适的应用场景就是解决同一把锁在同一线程内多次加锁的问题,这正好解决我们在分析NSLock这篇文章时的死锁问题。
- (void)viewDidLoad {
[super viewDidLoad];
self.recursiveLock = [[NSRecursiveLock alloc] init];
[NSThread detachNewThreadSelector:@selector(testRecursiveLock1) toTarget:self withObject:nil];
}
- (void)testRecursiveLock1 {
[self.recursiveLock lock];
NSLog(@"testRecursiveLock1");
[self testRecursiveLock2];
[self.recursiveLock unlock];
NSLog(@"testRecursiveLock1: unlock");
}
- (void)testRecursiveLock2 {
[self.recursiveLock lock];
NSLog(@"testRecursiveLock2");
[self.recursiveLock unlock];
NSLog(@"testRecursiveLock2: unlock");
}
如上面的代码,如果我们使用NSLock就会造成死锁。这里我们使用NSRecursiveLock,打印结果如下:
可以看到我们加锁执行1,然后在执行2,2解锁后返回到1在进行解锁。
6.3.2 示例2
这里通过block的递归调用来演示NSRecursiveLock的作用。
- (void)testRecursive{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
for (int i= 0; i<100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
}
打印结果:
6.4 死锁
那么递归锁会有死锁吗?答案是肯定的,在实例2中的代码加个循环就会造成死锁:
- (void)testRecursive2{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
for (int i = 0; i < 2; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
testMethod = ^(int value){
[lock lock];
if (value > 0) {
NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
testMethod(value - 1);
}
[lock unlock];
};
testMethod(10);
});
}
}
此时将锁换成@synchronized:
- (void)testRecursive2{
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
static void (^testMethod)(int);
for (int i = 0; i < 2; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
testMethod = ^(int value){
// [lock lock];
// if (value > 0) {
// NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
// testMethod(value - 1);
// }
//
// [lock unlock];
@synchronized (self) {
if (value > 0) {
NSLog(@"current value = %d--- 线程%@",value, [NSThread currentThread]);
testMethod(value - 1);
}
}
};
testMethod(10);
});
}
}
此时就不会造成死锁了,因为@synchronized是对递归锁进行了一次封装,通过哈希表对存储锁对象,对已经加锁的对象不再加锁,而只是增加lockCount。
7. atomic
我们在开发中经常会用到属性,也会经常写nonatomic,这是非原子属性,那么原子属性和非原子属性之间有什么区别呢?atomic到底是怎样实现属性的原子的性的呢?下面我们来探究一下。
7.1 寻找底层实现
由于atomic是关键字,我们不能通过点击跳转的方式去查看它的实现。因为声明属性这个语法是OC特有的,属性的本质也就是setter和getter,所以我们可以直接来到objc的源码中一探究竟。这里使用的是objc4-779.1源码。
7.1.1 setter
我们先看set方法中的实现:我们全局搜索setProperty
我们发现,所有setProperty方法最终都会调用reallySetProperty方法。其实我们可以写个属性赋,然后断点跟一下是否会调用reallySetProperty方法,这里就不验证了,肯定会调用的。我们直接看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);
}
该方法一共有7个参数:
self:这里存储的就是对象_cmd:方法名称,这里是setXxx:newValue:要赋的新值offset:指针偏移量。对象的指针就是isa的地址,当我们有多个属性的时候,会根据偏移量一个一个去内存地址中找这个属性atomic:是否是原子性,跟我们编码时写的atomic和nonatomic保持一致copy:是否是拷贝mutableCopy:是否是可变类型的拷贝
分析完参数我们来分析源码:
- 首先是判断偏移量是否为0,如果是0就是修改
isa,此处调用object_setClass后就直接返回 - 根据偏移量获取属性地址
slot - 根据是否是
copy和mutableCopy来拷贝newValue或者objc_retain(newValue) - 根据是否是原子性的进行处理,都会记录旧值后面进行释放,然后赋新值
- 如果是原子性的就通过
spinlock_t进行加锁进行赋值
7.1.2 getter
看完setter后我们在来看看getter,这里取值是调用的objc_getProperty方法,源码如下:
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
没啥多说的,同样是判断是否是atomic,不是就直接根据指针和偏移量取到的值直接返回,如果是atomic,就加锁spinlock_t获取值,并进行objc_retain,返回时是返回的自动释放对象objc_autoreleaseReturnValue(value),这里还有一句注释(为了提高性能,我们(安全地)在自旋锁之外发布自动释放。)
7.1.3 spinlock_t
关于spinlock_t在我的关于锁的第一篇文章中iOS 中的锁(1)中关于@synchronized的分析中有详细的讲解,可以移步那里看一看。
7.2 关于线程安全
那么原子属性真的安全吗?我们先来测试一下:
7.2.1 示例一
@interface ViewController ()
@property (atomic) NSInteger number;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
// 线程2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
}
@end
按照我们的预期,这里应该打印到200,但是只到199,我向上翻阅看到如下结果:
从上图可以看到9 被打印了两次。
7.2.2 示例二
下面我们来测试一下关于数组使用atomic时的线程安全。
@interface ViewController ()
@property (atomic, strong) NSArray *array;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 线程1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.array = @[@"aa", @"bb", @"cc"];
} else {
self.array = @[@"dd"];
}
NSLog(@"线程1: %@\n", self.array);
}
});
//线程2
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(@"取到的值是:%@",str);
}
NSLog(@"线程2: %@\n",self.array);
}
});
}
@end
这里我们看到直接由于数组越界导致程序崩溃了。
7.3 小结
由上面两个例子的打印结果可以看出atomic并不是线程安全的,它只是保证setter和getter方法内是安全的,一旦出了方法就需要外部控制了
比如我们的+1操作并没有加锁,所以就造成了值没有达到我们预期的结果,这里实际上就是两个线程同时取到了相同的值,在计算完后赋值的时候也都是一样的,这种情况多了值也就不是预期的了。
又比如在示例二中我们对数组的不断切换中,对数组取长度的时候是对的,但是在这之后数组长度变了,再去取值就会造成数组越界,导致程序崩溃。
所以说使用atomic修饰的属性时并不是绝对线程安全的,它只保证在setter和getter方法内是安全的,一旦出了方法,属性的安全性就得由程序员负责了。
8. dispatch_semaphore
关于dispatch_semaphore的应用在我的这篇文章中已经做了一些介绍。dispatch_semaphore属于GCD模块,源码实现自libdispatch库中,我们可以在Apple Open Source中下载各个版本的libdispatch源码。本文使用的是libdispatch-1173.40.5。
信号量作为锁是一种特例,当信号量的value传0和1时可以具有锁的特性。
信号量主要有下列几个函数:
dispatch_semaphore_create创建一个信号量dispatch_semaphore_wait等待信号量dispatch_semaphore_signal发送信号量
8.1 创建信号量 dispatch_semaphore_create
我们看看创建信号函数的定义:
/*!
* @function dispatch_semaphore_create
*
* @abstract
* Creates new counting semaphore with an initial value.
*
* @discussion
* Passing zero for the value is useful for when two threads need to reconcile
* the completion of a particular event. Passing a value greater than zero is
* useful for managing a finite pool of resources, where the pool size is equal
* to the value.
*
* @param value
* The starting value for the semaphore. Passing a value less than zero will
* cause NULL to be returned.
*
* @result
* The newly created semaphore, or NULL on failure.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
DISPATCH_NOTHROW
dispatch_semaphore_t
dispatch_semaphore_create(intptr_t value);
```*当两个线程需要协调时,传递0作为值非常有用
*完成某一特定事件。传递一个大于零的值是
*用于管理有限的资源池,其中池大小相等
*的值。
根据注释我们可以知道:
* 信号量是根据初始值`value`创建一个新的计数信号量
* 当两个线程需要协调完成一个特定的事件时,传0作为值非常有用
* 传递一个大于0的值值对于管理有限的资源池非常有用,其中池大小等于该值。
* 如果创建时传的值小于0则创建失败,对于创建失败的信号量会返回`NULL`
**dispatch_semaphore_create实现代码:**
```C
dispatch_semaphore_t
dispatch_semaphore_create(long value)
{
dispatch_semaphore_t dsema;
// If the internal value is negative, then the absolute of the value is
// equal to the number of waiting threads. Therefore it is bogus to
// initialize the semaphore with a negative value.
if (value < 0) {
return DISPATCH_BAD_INPUT;
}
dsema = _dispatch_object_alloc(DISPATCH_VTABLE(semaphore),
sizeof(struct dispatch_semaphore_s));
dsema->do_next = DISPATCH_OBJECT_LISTLESS;
dsema->do_targetq = _dispatch_get_default_queue(false);
dsema->dsema_value = value;
_dispatch_sema4_init(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
dsema->dsema_orig = value;
return dsema;
}
创建代码分析:
- 初始化dsema局部信号量变量
- 判断值是否小于0,如果小于0,则返回
DISPATCH_BAD_INPUT:#define DISPATCH_BAD_INPUT ((void *_Nonnull)0) - 为
dsema开辟内存并赋值 - 返回
dsema
8.2 等待信号量 dispatch_semaphore_wait
等待信号量函数定义:
/*!
* @function dispatch_semaphore_wait
*
* @abstract
* Wait (decrement) for a semaphore.
*
* @discussion
* Decrement the counting semaphore. If the resulting value is less than zero,
* this function waits for a signal to occur before returning.
*
* @param dsema
* The semaphore. The result of passing NULL in this parameter is undefined.
*
* @param timeout
* When to timeout (see dispatch_time). As a convenience, there are the
* DISPATCH_TIME_NOW and DISPATCH_TIME_FOREVER constants.
*
* @result
* Returns zero on success, or non-zero if the timeout occurred.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
根据注释我们可以知道:
- 等待信号量会导致信号量递减,如果结果值小于0,函数不会返回,直到等到一个信号发送(signal)
- 第一个参数:在此参数中传递NULL的结果是未定义的。也就是说第一个参数不能为空
- 第二个参数:超时时间,有
DISPATCH_TIME_NOW和DISPATCH_TIME_FOREVER两个常量可以用,也可以自定义。 - 返回值:成功返回0,超时返回非0
dispatch_semaphore_wait函数实现:
long
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0;
}
return _dispatch_semaphore_wait_slow(dsema, timeout);
}
等待信号量代码分析:
- 首先通过
os_atomic_dec2o去获取一个value,os_atomic_dec2o是一个一连串的宏定义,是对系统底层的一种封装。这里会对信号量-1 - 根据取到的值进行判断,如果
>=0函数直接返回0,即成功 - 如果上面没有返回则调用
_dispatch_semaphore_wait_slow函数进行处理并返回它的返回值
_dispatch_semaphore_wait_slow函数实现
static long
_dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema,
dispatch_time_t timeout)
{
long orig;
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
switch (timeout) {
default:
if (!_dispatch_sema4_timedwait(&dsema->dsema_sema, timeout)) {
break;
}
// Fall through and try to undo what the fast path did to
// dsema->dsema_value
case DISPATCH_TIME_NOW:
orig = dsema->dsema_value;
while (orig < 0) {
if (os_atomic_cmpxchgvw2o(dsema, dsema_value, orig, orig + 1,
&orig, relaxed)) {
return _DSEMA4_TIMEOUT();
}
}
// Another thread called semaphore_signal().
// Fall through and drain the wakeup.
case DISPATCH_TIME_FOREVER:
_dispatch_sema4_wait(&dsema->dsema_sema);
break;
}
return 0;
}
- 该函数首先会创建一个锁来保护我们的
dsema->dsema_sema - 接下来就是一个
switch分三种情况处理-
这里默认会通过
_dispatch_sema4_timedwait函数去休眠等待线程被唤醒,如果失败会贯穿到now分支。 -
对于
now会循环判断信号量的值是否小于0,如果是,则进一步处理,否则就会跳出,返回0,即成功 -
对于
forever,也是我们常用的,这里就是直接等待,直到线程被唤醒
-
8.3 发送信号量 dispatch_semaphore_signal
发送信号量函数定义:
/*!
* @function dispatch_semaphore_signal
*
* @abstract
* Signal (increment) a semaphore.
*
* @discussion
* Increment the counting semaphore. If the previous value was less than zero,
* this function wakes a waiting thread before returning.
*
* @param dsema The counting semaphore.
* The result of passing NULL in this parameter is undefined.
*
* @result
* This function returns non-zero if a thread is woken. Otherwise, zero is
* returned.
*/
API_AVAILABLE(macos(10.6), ios(4.0))
DISPATCH_EXPORT DISPATCH_NONNULL_ALL DISPATCH_NOTHROW
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema);
根据注释我们可以知道:
- 该函数是对信号量计数进行增加
+1,如果之前的值小于0,则这个函数在返回前会唤醒一个等待的线程 - 参数一:
dispatch_semaphore_t,不能为空 - 返回值:如果该函数唤醒了一个线程则返回非0,否则返回0
dispatch_semaphore_signal函数实现:
long
dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0;
}
if (unlikely(value == LONG_MIN)) {
DISPATCH_CLIENT_CRASH(value,
"Unbalanced call to dispatch_semaphore_signal()");
}
return _dispatch_semaphore_signal_slow(dsema);
}
- 该函数首先释放锁,对信号量计数
+1 - 如果计数大于0,直接返回
- 如果计数
value等于LONG_MIN则说明出现了指针异常,直接Crash报错 - 其他情况也就是
<=0,则调用_dispatch_semaphore_signal_slow函数发出一个信号唤醒一个等待中的线程。
_dispatch_semaphore_signal_slow源码:
long
_dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema)
{
_dispatch_sema4_create(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
_dispatch_sema4_signal(&dsema->dsema_sema, 1);
return 1;
}
8.4 小结
- 信号量是
GCD的dispatch_semaphore就是一个计数信号量,通过信号量的数量来管理线程,当计数大于0时线程会去执行任务,当计数小于0就会使线程休眠等待,当等待到一个信号后,会唤醒一个等待的线程去执行任务。 GCD信号量实际是调用了系统底层的信号量,对系统内核信号量的一层封装,感兴趣的可以去研究一下Linux内核的信号量。- 信号量的锁主要是在信号量计数为1或0时,通过
dispatch_semaphore_wait去等待线程执行时产生的锁的效果 - 这里的等待指的是在调用
dispatch_semaphore_wait的线程中等待,等待一个信号去唤醒这个线程 - 发送信号,是指在在其他线程中调用
dispatch_semaphore_signal函数增加一个信号量计数,如果计数<=0则会唤醒一个等待的线程
9. pthread
9.1 TLS
安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性。
该协议由两层组成: TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。
传输层安全性协议(Transport Layer Security),缩写为TLS,及其前身安全套接字(Secure Sockets Layer,缩写作SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。网景公司(Netscape)在1994年推出首版网页浏览器,网景导航者时,推出HTTP协议,以SSL进行加密,这是SSL的起源。IETF将SSL进行标准化,1999年公布第一版TLS标准文件,随后又公布了RFC5246(2008年8月)与RFC6176(2011年3月)。在浏览器、邮箱、即时通讯、VoIP、网络传值等应用程序中,广泛支持这个协议。主要的网站,如Google、Facebook等也以这个协议来创建安全连线,发送数据。目前已成为互联网上保密同学的工业标准。
SSL包含记录层(Record Layer)和传输层,记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密钥作为会谈密钥(Session Key)。这会谈密钥是用来将通信两方交换的数据做加密,保证两个应用间通信的保密性和可靠性,是客户与服务器应用之间的通信不被攻击者窃听。
9.2 TLS 线程相关解释
线程局部存储(Thread Local Storage,TLS): 是操作系统为线
程单独提供的私有空间,通常只有有限的容量。Linux系统下
通常通过pthread库中的 pthread_key_create()、pthread_getspecific()、 pthread_setspecific()、 pthread_key_delete()。
9.3 互斥锁
在Posix Thread中定义有一套专⻔用于线程同步的mutex函数。mutex用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
- 创建和销毁
POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁- 初始化:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) - 注销:
pthread_mutex_destroy()
- 互斥锁属性
- 互斥锁,分为递归锁和非递归锁。
- 对于递归锁使用的是
PTHREAD_MUTEX_RECURSIVE
- 锁操作
- 加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex) - 解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex) - 尝试加锁:
int pthread_mutex_trylock(pthread_mutex_t *mutex),在锁被占用是返回EBUSY,不会挂起线程等待。
- 加锁:
10. 读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源 进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。
如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。正是因为这个特性,当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须直到所有的线程释放锁。通常当读写锁处于读模式锁住状态时,如果有另外线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁⻓期占用,而等待的写模式锁请求⻓期阻塞。读写锁适合于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享,以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。
10.1 iOS中的的读写锁
读写锁适合于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享,以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。
10.1.1 初始化读写锁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
- 初始化成功则返回0, 出错则返回错误编号。
- 同互斥量以上,在释放读写锁占用的内存之前,需要先通过
pthread_rwlock_destroy对读写锁进行清理工作,释放由init分配的资源。
10.1.2 读写锁操作
- 读锁:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); - 写锁:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); - 解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
- 成功则返回0,出错则返回错误编号。
- 对于读锁和写锁是会阻塞线程的
- 尝试读锁:
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); - 尝试写锁:
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
- 成功则返回0,出错则返回错误编号。
- 非阻塞的获取锁操作,如果可以获取则返回0,否则返回错误的EBUSY。
10.2 自定义读写锁
10.2.1 自定义读写锁代码
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface MYRWLock : NSObject
// 读数据
- (id)my_objectForKey:(NSString *)key;
// 写数据
- (void)my_setObject:(id)obj forKey:(NSString *)key;
@end
NS_ASSUME_NONNULL_END
#import "MYRWLock.h"
@interface MYRWLock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问:
@property (nonatomic, strong) NSMutableDictionary *dataCenterDic;
@end
@implementation MYRWLock
- (id)init{
self = [super init];
if (self){
// 创建一个并发队列:
self.concurrent_queue = dispatch_queue_create("com.my.read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据字典:
self.dataCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
#pragma mark - 读数据
- (id)my_objectForKey:(NSString *)key{
__block id obj;
// 同步读取指定数据:
dispatch_sync(self.concurrent_queue, ^{
obj = [self.dataCenterDic objectForKey:key];
});
return obj;
}
#pragma mark - 写数据
- (void)my_setObject:(id)obj forKey:(NSString *)key{
// 异步栅栏调用设置数据:
dispatch_barrier_async(self.concurrent_queue, ^{
[self.dataCenterDic setObject:obj forKey:key];
});
}
@end
这里在写操作的时候,使用栅栏函数进行阻塞,读的时候不做限制。
10.2.2 使用示例
@property (nonatomic, strong) MYRWLock *rwLock;
- (void)testRWLock {
self.rwLock = [[MYRWLock alloc] init];
dispatch_queue_t queue = dispatch_queue_create("com.test.rwlock", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i<10; i++) {
dispatch_async(queue, ^{
NSString *str = [NSString stringWithFormat:@"rw_test%d",i];
NSLog(@"写%@---%@",str, [NSThread currentThread]);
[self.rwLock my_setObject:str forKey:@"rwKey"];
});
}
for (int i = 0; i<10; i++) {
dispatch_async(queue, ^{
NSLog(@"读 %@---%@",[self.rwLock my_objectForKey:@"rwKey"], [NSThread currentThread]);
});
}
}
11. 总结
-
在iOS中锁的这几篇文章中我们一个介绍了
NSLock、@synchronized、NSCondition、NSConditionLock、NSRecursiveLock、atomic、dispatch_semaphore七种锁。并且在@synchronized篇章中还介绍了os_unfair_lock(OSSpinLock); -
在这七种锁中
NSLock、NSCondition、NSConditionLock是对pthread_mutex的封装。 -
NSRecursiveLock是对pthread_mutex(recursive)的封装 -
@synchronized维护了一张哈希表,对同一对象加锁时采用lockCount在递归锁中可以避免死锁的问题 -
dispatch_semaphore是信号量为0和1时造成线程等待的锁现象 -
atomic是OC特有的修饰声明属性的关键字,只保证在set和get方法内的线程安全,在使用过程中不是绝对的线程安全。 -
其实还有一种锁
NSDistributedLock使用在Mac开发中,感兴趣的可以到Mac开发中探索一下
最后上一张大神的关于锁的性能图。这里OSSpinLock是一把自旋锁,在iOS10以后已经被苹果废弃了,用于替代它的是os_unfair_lock,原因是OSSpinLock虽然耗时断,但是占用资源多,这是一个已空间换时间的锁。
以上这些锁没有什么好坏之分,只是使用的环境各有不同而已,在开发中我们可以根据业务需求选择合适的锁进行使用。