iOS 中的鎖(1)
本文主要通過Objective-C
語言進行體現(xiàn)忆植,其實跟Swift
也差不多泣港。
本文從鎖的基本概念骑歹、NSLock售躁、@synchronized三個方面做了介紹坞淮。
1. 基本概念
鎖的存在主要就是解決資源搶奪的問題茴晋,在iOS中的鎖基本分為兩種,分別是互斥鎖和自旋鎖回窘,其實讀寫鎖也可以算一種诺擅,但是讀寫鎖也是一種特殊的自旋鎖。另外對于條件鎖啡直、遞歸鎖烁涌、信號量基本都是上層的封裝實現(xiàn)。
1.1 互斥鎖
自旋鎖避免了進程上下文的調(diào)度開銷酒觅,因此對于線程只會阻塞很 短時間的場合是有效的烹玉。
-
互斥鎖:顧名思義,就是相互排斥阐滩,是一種用于多線程編程中,防止兩條線程同時對同一公共資源(比如全局變量)進行讀寫的機制县忌。該目的通過將代碼切片成一個一個的臨界區(qū)而達成掂榔,也就是說一個線程獲得鎖后,其他線程在其釋放鎖之前都獲取不到鎖症杏。互斥鎖也分為兩種分別是遞歸鎖和非遞歸鎖装获。
- 遞歸鎖:對于互斥鎖我們可以使用遞歸的方式進行鎖定,簡答的說就是可以重新進行鎖定厉颤,在同一個線程釋放鎖前可以再次獲取鎖進行鎖定穴豫,并且不會造成死鎖。
- 非遞歸鎖:跟遞歸鎖相反逼友,不可以重新被鎖定精肃,必須等鎖釋放后才能再次獲得鎖
1.2 自旋鎖
- 自旋鎖:線程反復(fù)檢查鎖變量是否可用。由于線程在這一過程中保持執(zhí)行帜乞, 因此是一種忙等待司抱。一旦獲取了自旋鎖,線程會一直保持該鎖黎烈,直至顯式釋放习柠。簡單來說就是線程A獲取到鎖,在其釋放鎖之前照棋,線程B又來獲取鎖资溃,此時獲取不到,線程B就會不斷的進入循環(huán)烈炭,一直檢查鎖是否已被釋放溶锭,如果釋放,則能獲取到鎖梳庆。
1.3 互斥鎖和自旋鎖的區(qū)別
- 互斥鎖:當線程獲取鎖卻沒有獲取到時線程會進入休眠狀態(tài)暖途,等鎖被釋放時卑惜,線程會被喚醒,同時獲取到鎖驻售,繼續(xù)執(zhí)行任務(wù)露久,互斥鎖會改變線程的狀態(tài)。
- 自旋鎖:當線程獲取鎖但沒獲取到時欺栗,不會進入休眠毫痕,而是一直循環(huán)等待,線程始終處于活躍狀態(tài)迟几,不會改變線程的狀態(tài)消请。
1.4 使用場景
- 互斥鎖:由于其會改變線程的狀態(tài),這就需要內(nèi)核不斷的調(diào)度線程資源类腮,因此效率上比自旋鎖要低一些臊泰。理論上來說其實不適合使用自旋鎖的地方都可以使用互斥鎖。
-
自旋鎖:在等待鎖期間線程是活躍度蚜枢,所以這種活躍在一定時間內(nèi)是個死循環(huán)缸逃,會消耗更多的
CPU
資源,自旋鎖避免了進程上下文的調(diào)度開銷厂抽,因此對于線程只會阻塞很短時間的場合是有效的檀头。由于自旋鎖的線程活躍也就使得它在遞歸調(diào)用的時候會產(chǎn)生死鎖君丁。
1.4 死鎖
死鎖就是字面意思炕泳,鎖上了解不開棵癣,不解鎖就不能繼續(xù)執(zhí)行,基本就是兩個線程的相互等待藐守,最后誰也等不到挪丢,這里說明一下阻塞和死鎖的理解誤區(qū),阻塞就是不能繼續(xù)執(zhí)行了是線程內(nèi)的等待吗伤,死鎖是線程間的等待吃靠,本質(zhì)上是不一樣的。
1.5 其他鎖
- 條件鎖:就是條件變量足淆,當進程的某些資源要求不滿足時就進入休眠巢块,也就是鎖住了。當資源被分配到了巧号,條件鎖打開族奢,進程繼續(xù)運行。
-
信號量:其實信號量算不上鎖丹鸿,它只是一種更高級的同步機制越走,在互斥鎖中
semaphore
在僅取值0/1時的特例。信號量可以有更多的取值空間用來實現(xiàn)更加復(fù)雜的同步,而不單單是線程間互斥廊敌。
2. NSLock
NSLock
在分類中屬于互斥鎖铜跑,是我們在使用Objective-C
進行開發(fā)時常用的一種鎖÷獬海看了好多文章說NSLock
是非遞歸鎖
锅纺,確實NSLock
的遞歸上會引起阻塞或者崩潰,但是在同一線程內(nèi)NSLock
也可以再次加鎖肋殴,所以在這一點也不絕對囤锉。
2.1 NSLock 定義
我們點擊跳轉(zhuǎn)到NSLock
的定義處,源碼如下:
@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}bv
2.2 NSLocking 協(xié)議
從上一節(jié)中我們可以看到NSLock
遵守一個NSLocking
的協(xié)議护锤,協(xié)議定義如下:
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
我們可以看到協(xié)議中的lock
和unlock
方法就是我們常用的加鎖解鎖的方法官地。值得注意的是:-lock
和-unlock
必須在相同的線程中成對調(diào)用,否則就會產(chǎn)生未知的結(jié)果烙懦。
2.3 NSLock的其他方法
對于NSLock
還有另外兩個方法和一個屬性驱入,定義在源碼的下面,代碼如下:
// 嘗試獲取鎖氯析,獲取到返回YES沧侥,獲取不到返回NO
- (BOOL)tryLock;
// 在指定時間前獲取鎖,能夠獲取到返回YES魄鸦,獲取不到返回NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 鎖名稱,如果使用鎖出現(xiàn)異常癣朗,輸出的log中會有鎖的名稱打印
@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
2.4 NSLock 使用示例
這里我們模擬一個售票系統(tǒng)拾因,如果不加鎖的話就會導(dǎo)致一張票被賣多次的情況;加鎖后才能保證票數(shù)的準確旷余。
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{
//票數(shù)100張
static int number = 100 ;
while (1) {
// 線程加鎖绢记,提高數(shù)據(jù)訪問的安全性
[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的代碼后執(zhí)行
if ([lock tryLock]) {
NSLog(@"線程2");
[lock unlock];
} else {
NSLog(@"嘗試加鎖失敗");
}
});
}
打印結(jié)果:
根據(jù)打印結(jié)果我們可以看到,tryLock
返回NO后也不會阻塞線程正卧,還繼續(xù)執(zhí)行下面的代碼蠢熄。
2.4.3 lockBeforeDate
如果將2.4.2中的tryLock
換成[lock lockBeforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]
就會阻塞線程,它將在Date
前嘗試加鎖炉旷,如果在指定時間前都不能加鎖則返回NO签孔,加鎖失敗后跟上面打印是一致的。如果加鎖成功打印結(jié)果如下:
打印結(jié)果:
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
窘行,在同一線程內(nèi)如果沒有解鎖就再次加鎖的話就會造成死鎖饥追。這里就是testLock2
等待testLock1
解鎖,而testLock1
也在等testLock2
解鎖罐盔。
2.5 NSLock 底層實現(xiàn)
通過上面的NSLock
定義我們可以知道NSLock
是在Foundation
庫中實現(xiàn)的但绕,但是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?
}
根據(jù)源碼我們可以看到NSLock
是對pthread
互斥鎖(mutex)的封裝捏顺,我們可以看到在lockBbeforeLimit
方法中會調(diào)用timedLock
這個方法六孵,這也是在Date
前實現(xiàn)加鎖的真正實現(xiàn),我們跳轉(zhuǎn)到該方法(PS:在源碼中有Windows平臺的實現(xiàn)幅骄,這里我們就不看了劫窒,直接看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
}
- 首先設(shè)定超時時間
- 然后開啟
while
循環(huán) - 在循環(huán)內(nèi)通過
pthread_cond_timedwait
函數(shù)進行計時等待,線程進入休眠 - 如果超時直接返回
false
- 如果等待沒超時昌执,并在這期間鎖被釋放烛亦,則線程被喚醒,再次通過
pthread_mutex_trylock
函數(shù)嘗試獲取鎖 - 如果獲取成功則返回
true
- 如果沒有超時懂拾,但是別喚醒后也沒有獲取到鎖(被其他線程搶先獲得)煤禽,則重新計算超時時間進入下一次
while
循環(huán)
2.6 小結(jié)
至此我們對NSLock
的分析就完畢了,總結(jié)如下:
- 在使用
NSLock
的時候lock
和unlock
方法的使用時成對出現(xiàn)的 - 切記不要在同一線程中連續(xù)加鎖又不解鎖岖赋,防止死鎖的出現(xiàn)
-
tryLock
方法不會阻塞線程 -
lockBeforeDate
方法會在超時前阻塞線程
3. @synchronized
@synchronized
是我們在使用Objective-C
開發(fā)時使用最多的一把鎖了由于代碼簡單且方便實用深得廣大開發(fā)者喜歡檬果。但是很多人并不知道@synchronized
底層實現(xiàn)是個遞歸鎖,不會產(chǎn)生死鎖唐断,且不需要程序猿手動去加鎖解鎖选脊。下面我們就慢慢揭開@synchronized
的面紗。
3.1 @synchronized 實現(xiàn)探索
由于@synchronized
是關(guān)鍵字脸甘,我們并不能直接查看它的具體實現(xiàn)恳啥。此時我們編寫如下代碼:并添加斷點,通過匯編進行初步探索
要想查看匯編則需要在Xcode->Debug->Debug Workflow->Always Show Disassembly
選中丹诀。
由上面的匯編代碼我們可以看到在NSLog
的上下分別有objc_sync_enter
和objc_sync_exit
的調(diào)用(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;
}
通過注釋我們可以看出該函數(shù):
- 首先在
objc
上同步 - 如果需要就分配一個與
objc
相關(guān)聯(lián)的遞歸鎖 - 一旦獲得鎖就返回
OBJC_SYNC_SUCCESS
其實代碼上也跟上面說的一致,只是還有些細微的處理:
- 如果鎖定的對象是空的橄妆,就不進行任何加鎖操作
- 這里的
result
一直是OBJC_SYNC_SUCCESS
衙伶,所以說就算因為對象obj
為空導(dǎo)致加鎖失敗也不會阻塞線程,而是直接向下執(zhí)行害碾。 - 如果
obj
不為空痕支,那么回調(diào)用id2data
函數(shù)獲取一個SyncData
類型的對象 - 這里如果獲取不到鎖會通過
data->mutex.lock();
阻塞當前線程,等其他線程釋放鎖后繼續(xù)向下執(zhí)行蛮原。
3.2.2 關(guān)于加鎖(id2data)進一步探索
id2data
有兩個參數(shù)卧须,第一個是鎖定對象obj
,第二個ACQUIRE
這里是個枚舉值ACQUIRE
的意思是加鎖,其實還有兩個分別是RELEASE
意思是解鎖和CHECK
檢查鎖的狀態(tài)花嘶。
我們在來看看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
是一個結(jié)構(gòu)體,擁有4個成員:
-
nextData
:指向下一個節(jié)點椭员,像極了一個鏈表的節(jié)點 -
object
:一個OC
對象车海,其實它就是保存被鎖對象的obj
的。 -
threadCount
:記錄線程數(shù)隘击,其實就是調(diào)用synchronized
代碼塊的線程數(shù) -
mutex
:遞歸鎖侍芝,這就底層實際的鎖,通過調(diào)用它的lock()
方法實現(xiàn)加鎖操作埋同。
id2data函數(shù)分析
由于id2data
代碼比較多州叠,這里我們通過折疊代碼先來簡單看看
- 首先第一塊就是初始化一個局部的鎖,以及一個
SyncData
指針凶赁,用作在鏈表中查找用也就是鏈表的頭指針咧栗,還有一個result
用作存儲返回結(jié)果 - 第而部分就是在單線程的緩存中進行查找了(查找是為了查找加鎖對象)
- 下一步分就是在單條線程上沒有查找到則需要進行全局緩存的查找
- 接下來就是都沒有查找到,就說明是第一次給該
OC
對象加鎖虱肄,所以要進行第一次存儲 - 最后就是對查找對象的處理了
下面我們來一步一步的分析致板,首先看看前三行代碼
第一行就是獲取一個鎖,這是個局部變量咏窿,在本函數(shù)內(nèi)需要使用的鎖斟或,看名字spinlock_t
是個自旋鎖,那么我們來看看它的實現(xiàn)的集嵌,源碼如下:
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;
根據(jù)上面的注釋缕粹,也就是下面這句,os_unfair_lock
是用來替代OSSpinLock
這個自旋鎖的互斥鎖纸淮,不會自旋,在內(nèi)核中等待被喚醒亚享。所以說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
的定義知道它是一個鏈表結(jié)構(gòu)侈沪,所以說這個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) { }
};
可以看到這個宏是個全局靜態(tài)變量sDataLists
晚凿,以obj
為索引獲取值亭罪,獲取到的對象類型為StripedMap<SyncList>
,同時對其取data
取地址進行返回歼秽。對于StripedMap
是一個類应役,如下圖:
- 通過圖中的第834行代碼我們可以看到,
p
就是我們傳進來的對象obj
,從數(shù)組中取值返回 - 數(shù)組的索引通過
indexForPointer
這個方法計算得出箩祥。 - *算法是取出對象地址院崇,然后對地址右移四位 異或 上地址右移9的結(jié)果,然后對
StripeCount
取余袍祖,這StripeCount
在我們的iPhone
上只有8
位底瓣,在類的一開始就定義了。 - 這個
8
就是iPhone
真機上所能使用哈希表的大小 - 對于哈希表就可能存在哈希沖突蕉陋,但是在這里是通過這個方法去取鏈表捐凭,這個哈希表的數(shù)據(jù)是某條鏈表的頭結(jié)點,并且每條鏈表中還會有很多個節(jié)點凳鬓,每個節(jié)點又保存了和不同對象相關(guān)聯(lián)的鎖茁肠。
第三行代碼就是定義了一個result
值為NULL
,是id2data()
需要返回的結(jié)果村视,就沒啥說的了官套。
接下來是在單線程中查找,代碼如下:
- 首先我們看到的是一個宏定義
SUPPORT_DIRECT_THREAD_KEYS
它的值是1蚁孔,并且在#if defined(__PTK_FRAMEWORK_OBJC_KEY0)
下才會被定義為1奶赔,雖然具體什么意思不太明了,但是大概能看出來是在objc
的某個環(huán)境下使用杠氢,也正是我們需要研究的 - 首先看注釋就能很好的理解該分支是在單線程中查找緩存
-
fastCacheOccupied
標記當前線程的快速緩存是否已被占用 - 通過
SYNC_DATA_DIRECT_KEY
和tls_get_direct
函數(shù)從線程的快速緩存中取出一個SyncData
節(jié)點站刑,其實單線程私有數(shù)據(jù)只保存一個節(jié)點的地址。 - 如果這個節(jié)點沒有值也就拜拜了鼻百,直接跳過绞旅,如果有值就標記
fastCacheOccupied
為YES,即使這個節(jié)點中沒有我們要找的被鎖對象 - 如果快速緩存中恰好是當前對象關(guān)聯(lián)的鎖温艇,那么對這個鎖的計數(shù)
+1
因悲,如果是解鎖就-1
(這些計數(shù)是當前線程的私有數(shù)據(jù),其他線程訪問不到) - 如果找到勺爱,則會返回晃琳,如果找不到就會到下一流程進行處理了
在單線程緩存中查找不到后,就會來到下面的全局緩存中進行查找
- 首先通過
fetch_cache
函數(shù)獲取整體的緩存對象SyncCache
SyncCache 和 SyncCacheItem 結(jié)構(gòu)體:
兩個結(jié)構(gòu)體實現(xiàn)如下琐鲁,詳見注釋卫旱,對于SyncCacheItem
就一看就明了了。
typedef struct SyncCache {
unsigned int allocated; // 保存`SyncCacheItem`的總數(shù)
unsigned int used; // 保存使用的數(shù)量
SyncCacheItem list[0]; // 緩存鏈表頭結(jié)點地址
} 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
數(shù)據(jù)围段,_objc_pthread_data
也是一個結(jié)構(gòu)體 - 其實在這個方法中就是通過
_objc_fetch_pthread_data
去獲取這個data
顾翼,(函數(shù)內(nèi)還調(diào)用了好幾步)這里就不一層一層的去分析了,感興趣的可以自己點擊跳轉(zhuǎn)就跟一下 - 這個方法其實重要的還是做了個擴容奈泪,初始值是4适贸,如果不夠了就進行2倍擴容
如果來到下面這段代碼就說明我們在任何緩存中都沒有找到當前對象的鎖灸芳,說明是第一次給這個對象加鎖。
- 首先就是使用一開始定義的局部鎖變量進行加鎖
- 然后通過局部變量
p
遍歷循環(huán)鏈表 - 這里在遍歷過程中如果找到了要加鎖的對象取逾,但是能到這里說明這個對象不被任何線程占用耗绿,所以直接使用這個對象就可以了,不需要重新開辟內(nèi)存空間插入鏈表
- 如果沒有的話就通過變量
firstUnused
記錄第一個沒有使用的節(jié)點 - 如果不是加鎖到這里就可以
goto done 了
- 最后如果
firstUnused
不為空對result
進行一系列賦值操作后就可以goto done 了
- 過了這個代碼塊砾隅,說明是第一次加鎖使用緩存哈希表误阻,則創(chuàng)建個新的節(jié)點放在表頭(也有可能是某一條鏈表中節(jié)點都被使用,重新開啟一條鏈表晴埂,也就是上面提到的8條的哈希表中的一個)究反。
最后我們goto done
在done這個模塊主要是對result的一些處理
- 首先是解鎖
- 然后判斷
result
如果是空的話就直接調(diào)用下面的返回了,返回了一個NULL
- 如果是解鎖的話就直接返回
nil
儒洛,如果也不是加鎖就報錯了精耐,如果加鎖對象不匹配也會報錯,其實這塊就是容錯處理琅锻,基本不會來到 - 下面就是對快速緩存的處理卦停,如果線程的快速緩存沒有被占用就存儲到快速緩存中,如果被占用就將節(jié)點存儲到全局緩存中
關(guān)于快速緩存:前面分析的時候無論在線程緩存中是否找到被鎖的對象(前提是線程快速緩存存在)fastCacheOccupied
都會被置為YES
恼蓬,也就是說線程私有數(shù)據(jù)的快速緩存只緩存一次惊完,且只保存第一次的這一個節(jié)點指針。我覺得就是你鎖了一次处硬,下次在鎖的概率很大小槐,使用頻率也會超級高,因為在鎖定一個對象的時候大多情況都是多線程操作這個對象荷辕,在短時間內(nèi)操作頻率足夠高凿跳,如果不高的話可能也不至于用鎖,還有可能該對象的同步鎖已經(jīng)被其他線程緩存到其他線程的私有數(shù)據(jù)了疮方,當前線程又無法訪問其他線程的私有數(shù)據(jù)控嗜,如果替換的話,會重復(fù)緩存骡显。
3.2.3 objc_sync_exit
關(guān)于解鎖我們也是直接看源碼了疆栏,代碼如下:
// 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
是否有值石挂,沒值就是對空對象解鎖直接返回就好了 - 如果有值就經(jīng)過
id2data
返回要解鎖的節(jié)點 - 如果沒返回節(jié)點就給
result
賦值error
然后返回 - 如果返回值了就調(diào)用
tryUnlock
去解鎖 - 如果解鎖失敗也是給
result
賦值error
然后返回 - 其他情況返回一開始定義的
result
的默認值就好了
3.3 @synchronized 總結(jié)
3.3.1 注意事項
-
@synchronized
不能鎖定和解鎖空對象,所以在使用的時候一定要注意鎖定對象不能是空值否則就會出現(xiàn)加鎖失敗的情況险污,達不到我們預(yù)期的效果(空對象也不會影響代碼的執(zhí)行) -
@synchronized
對于指針不斷變化的屬性或者成員變量可能不符合我們想要通過鎖來解決問題的初衷痹愚,所以在使用它的時候要特別注意對加鎖對象生命手氣和指針指向的變化
3.3.2 @synchronized
-
@synchronized
屬于遞歸鎖富岳,在同一線程內(nèi)可重新加鎖,在其內(nèi)部有個持有鎖的計數(shù)器 -
@synchronized
加鎖是調(diào)用的int objc_sync_enter(id obj)
函數(shù) -
@synchronized
解鎖是調(diào)用的int objc_sync_exit(id obj)
函數(shù) - 在上面的兩個函數(shù)中都主要通過
id2data
這個函數(shù)來存儲和獲取SyncData
鎖對象- 在
id2data
中首先在線程的快速緩存中查找鎖對象節(jié)點 - 如果找不到就去全局緩存中查找
- 如果全局緩存中沒有就說明是第一次鎖定該對象拯腮,會從全局緩存的鏈表中遍歷找到第一個空閑節(jié)點存儲該對象關(guān)聯(lián)的新節(jié)點
- 如果沒找到空節(jié)點就重新開辟一個新的鏈表窖式,將該對象關(guān)聯(lián)的節(jié)點存儲為鏈表的表頭
- 最后將節(jié)點的結(jié)果存儲到線程的私有數(shù)據(jù)中,并保存早全局緩存中
- 對于解鎖跟以上步驟一致动壤,只是通過傳入的加解鎖標志
why
(枚舉類型)進行不同的處理萝喘,最后返回待解鎖節(jié)點,通過調(diào)用mutex.tryUnlock();
進行解鎖 - 在沒條線程中維護這鎖的計數(shù)琼懊,沒加鎖一次計數(shù)加一阁簸,解鎖則減一
- 關(guān)于全局緩存,實際上是一張哈希表哼丈,通過對鎖對象
obj
的指針地址進行哈希計算得出索引 -
hash
表的的Value
是鏈表第一個節(jié)點(SyncData
)的地址启妹,每個鏈表中每個節(jié)點對應(yīng)不同的鎖對象(SyncData
)
- 在