iOS底层学习 – 多线程之中的锁🔐


通过之前篇章的学习,我们对整个GCD从使用到原理,都有了一定的理解。这篇主要讲解一下iOS开发中的锁是什么情况

系列文章传送门:

☞ iOS底层学习 – 多线程之基础原理篇

☞ iOS底层学习 – 多线程之GCD初探

☞ i! Z K ] W p H G iOS底层学习 – 多线程之GCD队列原理篇

☞ iOS底p * %层学习 – 多线程之GCD应用篇

☞ iOS底1 5 * w y D / r i层学习 – 多线程之GCD底层原理篇

基础小概念

什么是锁

锁 — 是保证线程安全常见的同步工具。锁是一种非强制的机制,每一个线程在访问数据或者资源前,要先获取(Acquire) 锁,m A r I F J 6 ; =并在访问结束之后释放(Release)锁。如果锁已经@ k 3 u m h被占用,其它试图获取锁的3 / J 1 ~ 8 P o线程会等待,直到锁重新可用。

锁的作用

前面说到了,锁是用来保护线程安全的工具。

可以试想一下,多线程编程时,没有锁的情况 — 也就是线程不安全。

当多个线程同时对一块内存发生读和写的操作,可能出现意料之外的结果:

程序执行的顺序会被打乱V b i Q # 9,可能造成提前释放一个变量,计算结果错误等情况。

所以l I D V 7 Q X d R我们J T 4需要将线f f + *程不安全的代码 “锁” 起来。保证一段代码或者多段代码操作的原子性,保证多个线程对同一个数据的访问 同步 (Synchronization)。

锁的分o O F V

锁的分类方式` – p 3 D ` *,可以根据锁的状态,锁的特性等进行不同的分类v ! ,很多锁之间其实并不是并列的关系,而是一种锁下的不同实现。可以看这篇d ) ? i a % ,文章JAVA中锁的分类

互斥锁与自旋锁

互斥锁:是⼀种⽤于多线程编程中,防⽌两条线程同时对同⼀公共资源(⽐ 如全局变量)进⾏读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。 互斥锁又分为递归锁和非递归锁。

  • 递归锁:/ 5 D _ z 5 M可重入锁,同一个线程在锁释放前A ^ = A 9 X可再次获取锁,即可以递归调用。
  • 非递归锁:不可重入,必须等锁释放后才能再次S V | J获取锁。

⾃旋锁:线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏, 因此是⼀种忙等待。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释 放⾃旋锁。 ⾃旋锁避免了进o & u| b 3 s D上下⽂的调度开销,因此对于线程只会阻塞3 l P R F t I 2 z很 短时间的场o x O合是有效的。

互斥锁与自旋锁区别:

其实就是线程l U . ( *的区别,互斥锁在线程获取锁但没有获取到时,线程会进入休眠状态,等锁被释放时,线程会被唤醒,而自旋锁的线H 0 $ c V 4程则会一直处于等待状态,忙等待,不会进入休眠。

自旋锁

1. OSSpinLock

相信大家都拜读过这片文章->不再安全的 OSSpinLock。总结来说,自旋锁之所以不安全,是因为由于自旋锁获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转。

r ^ c 9 | w OSSpinL# = 0 u N & W o $ock 忙等的机制,就可能造成高优先级一直 run1 9 k V Tning ,占用 CPU 时间片。而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况。

2. atomic

在面试中,我们经常遇到关于atomic相关的问题,总结来说主要是两个方面,一c x a J s S [个是atomic的底层原理是怎样的,另一个是使用atomic是否就能保证线程安全。

关于底层原理,我们还是来看源码进行探索。通过源码,我们可以发现,在0 : 5 j E h e X方法的setget方法中,会有是否是atomic的判断,如果不是的话,则直接进行赋值,如果是的话,会加一个8 & r s W n zspinlock_t的锁,这个锁保证了对属性读写的安全。

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t oX E 5 I o `ffset)
{
reallyc D e ? R 3 l 7 nSetProperty(self, _cmd, newValueZ i 0 , C e m x, offset, true, false, false);` { 
}
sq q c H h J Q f dtatic inline void reallyS 8 1 8 O ~ } r 3SetPropery v p *ty(id sv _ * [ I @ ! oela & L Uf, SEL _cmd, id newValH P W R W |ue, ptrdiff_t offset, bf . } ? { U = Rool atomic, bool copy, bool mutabG _ N { , M $ v nleCopy)
{
// ...
if (!atomic) {
// 不是 atomic 修饰
oldValue = *slot;
*slot = newValue;
} else {
// 如果是 atomic 修饰,加一把同步锁,保证 setter 的安全
spinlock_t& sloi } Mtlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slC [ A i o Pot = newValue;
slotlock.unlock();
}
}
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// ...
// 非原子属性,直接返回值
if (!atomic) return *slot;
// 原子属性,加同步锁,保证 getI 5 o M ~ter 的安全
spinlock_t& slotlock = PropertyLocks[s] 3 5 1 -lot];
slotlock.lockE q H();
id value = objc_retain(*slot);
slotlock.unlock();
}

既然atomic是保证setget方法安全的,那是不是就说明其线程安全呢?其实并不是的,这只能保证该属性在单一线程上是安全的,如果是有很多的线程对该属性进行同时的操作,那么就不能保证其数据H 0 / 2 b安全了.比如下面的代码,通过结果我们可以看到,并没有起到加锁的效果。

    //Thread A
dispatch_async(dispaD ? @ 7 * 6 2 Ltch_get_global_queue(0, 0), ^{
for (int i = 0; i& , _ c < 100; i ++) {
self.num = self.num + 1;
NSd h 2 . & B pLog(@"Thread A:%ldn",self.num);
}
});
//Thread B
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 100; i ++) {
NSLog(@"Thread B:%ldn",self.num);
}
});
-----------------------------d ) B P i :--------u : 4 H ?------------------------
Thread A:1
Threa_ & |  -d B:1
Thread B:2
Thread A:2

3. 读写锁

读写T _ h锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对z ) Y . ?A 2 @ p t o享资源进⾏写操作。这种锁相对于⾃旋锁⽽⾔,能提⾼并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最⼤可能的读者数为实际的逻辑CPU数。

  • 写者是排他性的,⼀个读写锁同% Y x H a t / 时只能有⼀个写者或多个读者(与CPU数相关),但不能同时既有读者⼜有写者。在读写锁保持期间也是抢占失效的。

  • 如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴k G d h V 8 2 !即获得该读写锁,} } _否则读者q @ F b R # 0必须⾃旋在那⾥,直到写者释放该读写锁。

具体用法如下,不过在日常开发中较少使用

// 需要~ h A o Q _ % f e导入头文件
#include <pthread.h>= E W ~ [ ,;
pth: S - 7 fread_rwlock_t lock;
// 初始化锁
pthread_rwlo} a * J / ` @ zck_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthreE b  y . L Z :ad_rwlock_wrL # lock(&lo= L l , Jck);
// 写-尝试加锁
pthread_rwlock_trywrln 2 Uock(&lock);
// 解锁
pthread_rwlock_unlock(&loc? O O d dk);
// 销毁
pthread_rwlock_destroy(&lock);

我们可以使用并发队列+dispatch_barrie{ B cr_async来实现一个类似的读写锁

########### .h文U . P ] Y = { 1 k
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface WY_RWLock : NSObject
// 读数据
- (id)wy_objectForKey:(NSString *)key;
// 写数据
- (void)wy_setObject:(id)obj forKey:(NSString *)key;
@end
NS_ASSUME_NONNUL| * ) g 2 m tL_END
########### .m文件
#import "WY_RWLock.h"
@interface WY_RWLock ()
// 定义一个并发队列:
@property (nonatomic, strong) dispatch_qe u lueue_t concurrent_queue;
// 多个线程需要数据访问
@prop% ! Kerty (nonatomiv 3 nc, strong) NSMutableDictionary *dataCenterDic;
@end
@implementation WY_RWLock
- (id)init{
self = [super init];8 F ~ F ^ K
if (self){
// 创j + @ ! O建一个并发队列:
self.concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUs ` : q 3 } N 2 ~EUE_CONCURRENT);
// 创建数/ r `据字典:
self.dataCenterDiY h E 0 &c = [NSMutableDictionary dictionary];
}
re{ U g W i 6 * Qturn self;
}
#pragma mark - 读数据
- (id)wy_objectFor% N e B T J / ~ pKey:(NSString *)key{
__block id obj;
// 同步读取指定数据:
dispatch_sync(self.concurrent_queue, ^{
obj = [sel h r 6 ` d S Q xlf.dataCev t @ f DnterDiu i | d x t 7c objectForKey:key];
});
return obj;
}
#praE h g x B / ] 3 bgma mark - 写数据
- (void)wy_setObje} Z E C } * K 6 lct:(id)obj forKey:(NSString *)key{
// 异步栅栏调用设置数据:
dispatch_barrier_async(self.concurrent_queueY i h m 7 5 Q 9, ^{
[self.dataCenterDic setObject:obj forKey:key];
});
}
@end

互斥锁

互斥锁为什么安全

因为互斥锁出现优先级反转后,高优先级的任务不会忙等。因为处于等待状态的高优先级任务,没有占用时间片,所以低优先级任务一般都能r D ` *进行j Q 1 w U u下去,从而释0 H #放掉锁。

互斥锁性能

iOS底层学习 - 多线程之中的锁🔐

1.j 3 K @synchronized

@synchronized的使用非常简单,代码如下,传入一个想要加锁的对象,在其中执行加锁的相8 s o O n B关逻辑即可。

@synchronized (obj) {}

那么其底层逻辑是如何实现的呢,我们可以看一下@synchronized的源码,通过打断点^ . G 2 C R,查看其汇编源码,发现@sy- R Lnchronized就是实现了objc_sync_c L T 7 ! M ?enterobjc_sync_exit两个方法,也就是说是5 k z ! x G s v通过这两个方J N a o R I ^法来实现加锁和解锁操作的。通过符号断点,我们可以知道u l / p J / _ v其代j 5 F . , C ( % u码在obj8 ] = a Zc源码中。

首先注意enterexit中都首先对obj是否为nil做了判断,如果obj为空时,则不会进行加锁和解锁的相关操作。所以在使用! ) ` T t N一定要注意传入的值a W # 5 I N 4 k会不会被析构,造成传入值为空的情况L H X D e %,从而加锁失败

比如在线程异步同时操作同一个7 z 6对象时,因为递归锁会不停的alloc/release,这时候某一个对象会可能是m ) [ c Lnil,从而导致加锁失败

int objc_sync_entH 0 / 2 %er(id obj)
{
int result = OBJC_SYNC_SJ M z } . s Z Q #UCCESS;
if (ob/ U f o * ] 2 + 5j) {
Sn L . yncDato / ! o Ka* data = id2data(obj, ACQUI^ W ? W vRE);
assert(data);
dd 1 6ata->mutex.lock();
} else {
// @synchronized(nil) does nothing// 如果obj为空,则不进行加锁操作
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBn v T M y 1 | G XUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objca 3 t = R f_sync_) . . lnil();
}
return result;
}
----------------------------------------------------------------------------------c ( ;---------h o p H----------------------------
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJCM * $ u ; C_SYNC_Nz C T % w * +OT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
}} ; B else {
✅// 如果obj为空,则X R F ( A H } i不进行解锁操作
// @synchronized(nilp 7 8 V |) does nothing
}
return result Q h x u p kt;
}

在具体的实现逻辑中,我们可以看到通过idS Q ; # &2data方法,对obj进行了捕获和释放的[ L 8操作,并生成了一个SyncDataA L x $ D j C ` n型的对象。我们发现SyncData是一个结构体,而且有一个SyncE _ ?Data类型的nextData7 T I M A M量,y M 0 j Y N (指向下个数据,所以我们可以知道SyncData是一个链表结构中的一个元素。所以这是一个递归锁。

  • nextD1 ` ; & Y sata指的是链表o f 3 G [ p Z | b中下一个元素
  • object指的是传入需要加解锁的对象
  • threadCount就表示当前的线程数量
  • mutex即对象所关联的锁
typedef struct alignas(C) k /acheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtrp ? | E F B&l9 & b ;t;objc_object> object;
int32_t threadCount;  // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;

; _ ( w } Y B 2解了SyncData结构后,我们继续来查看源码,由于源码比较长,所以我们分模块俩讲解。

1.1s ` q J i M E 准备SyncData

我们可以看到会会通过LOCK_FOR_OBJLIST_FOR_OBJ取出objecN . 2 ~ [ k Lt所对应的lockplistp

static SyncData* id2data(id object, enum usage why)
{
spinlock_t *le P X ] t G 0 ockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
SyncData* result0 I ! t 9 = NULL;
...
}

既然我们在任何地方都可以直接通过调用方法来使用,那么说明Z } d t F ^ X N .底层必然维护着一套内部的存储。通过代码我们m O a L _ o 7 n也可以看出,系统在底层维护了一个哈希表,里面存储了SyncList结构的数据,而SyncList是一个结构体,包含一个SyncData / * r f &的头结K ( g b点和一个s# G U P 1pinlock_t锁对象

------------:  @ = y k R ) K-------------------------R d X v---------------------X C a y Q q-----------------------------------------Y h +--------------------
struct SyncList {
SyncData *data;
spinlock_t lock;
constexpr SyncList() : data(nil)[ 1 1 4 E, lock(fe { s $ 6 { Bork_unsafe_lock) { }
};
--------------------------------------------------------------------------------& F M 0 ^ 6 y A /---------------------------------------
// Use multiple parallel lists to dv ^ 5ecrease contention among unrelated oB i wbjects.
#` K m o Ndefine LOCK_FOR_OBJ(obj)z  8 v sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLe a 3 ^ 8ists[obj].data
static StripedMap<SyncList4 L g y q y 0 ? 3> sDatau W C d oLists;

1.2 快速检查线程缓存

此步操作会通过tls封装的相关p6 N I c 1 %thead操作线程的相关增删改查方法,获取到单个线程中缓存的SyncData数据,并进行快速查询和缓存

static SyncData* id2data(id object, enum usage why)
{
.x C % @..
#if SUPPORT_DIRECT_THREAD_KEYS
// Ch@ & Y  c leck per-thread single-entry fast cache for matching object// 检查每线程单项快速缓存中是否有匹配的对象
bool fastCacheOccupied = NO;
✅// 通过tls相关封装的pthead方法获取是否有再底层存储的SyncData
SyncDw = 0 ?ata *data = (SyncT ) t 2 C xData *)tls_g? z r 0 # g set_direct(SYNC_DATA_DIRECT_KEY);
if (data){ N P d d H q {
fastCacheOccupied = YES;
✅// 如果获取到的数据和传入数据相同
if (data->object == object) {
// Found a match in fast cacG [ d k #he.
uintptr_t lockCount;
result = data;
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
if (result->threadCount &l* y 0 2 [ { G E dt;= 0  ||  lockCount <= 0) {
_objc_fatal("id2data fastcache is bug1 M agy");
}
switch(why) {
case ACQUIm  dRE: {
// 如果是 entry,则对 lockCounw 6 Z + ^ / } u Tt 加 1,并通过 tls 保存
lockCount++;
tls_set_direct(SYc W h W m jNC_COUR K a j s d A n NT_DIRECT_KEY, () ` Qvoid*F , k ] [)lockCount);
break;
}
case RELEASE:
// 如果是 exZ 4 O A @ ) Z @it,则对 lockCount 减 1,并通过 tls 保存
lockCount--;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lock` . = m A ] P - $Count)A M r W z p 0 c 7;
if (lockCount == 0) {
// remove from fast cache
// 如n c { j T M a ~果 lockCount 为 0,则从高速缓存中删除
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULu Y @ = QL);
// atomic beca^ x J ? x S ) %use may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCou- 5 . n 2 ] [ o *nt);
}
break;
case CHECK:
/C 5 + c/ do nothing
break;
}
return result;
}
}
#endif
...
}

1.3 检查有锁线程中的缓存

这步操作是检查所有线程中的缓存

static SyncData* id2data(id objC w j  ] ( _ect, enum usage why)
{
...
// Check per-th{ _ M v - W X 9read cache of already-owned locksO q 5 H j Y w 1 forV 1 ^ matching ob| u Z %ject
// 检查已拥有锁的每个线程高速缓存中是否有匹配的对象
SyncCache *cache = fetch_cachX L ? y U te(NO);
if (cache) {
unsig( 6 k K ned int i;
for (i = 0; i < cache( . ~ 2 ) 6->Z S : G f;used; i++) {
SyncCacheItem *item, ? ? f ( 2 v c o = &cache->list[i];
if (item->data->object != object) continue;
// Found a mat/ U = ) Y / #ch.
result = item-&gi b 8t;data;
if (result->threadCount <= 0  ||  item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why)F 2 o {
case ACQUIRE:
item->lI B l 0 K = ockC{ t a e 4ount+n A @+;
break;
case RELEASE:
item->lockCount--;
if (item->| n T O y 0 X P xlockCount == 0) {
// remove from per-thread cachx N y Me
cache->list[i] = cache->list[-o _ : 1 w 1-cache->used];
// atH O d Fomic because ma= 0 A : a y a 2y collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do * a J ^ xo nothing
break;
}
return resultd { t + h ? { _ ^;
}
}
...
}

1.4 全局哈希表查找

如果上述两步中,单个线程和已经锁住的线程中的缓存数据都没有找到的话,那么就会来到此步,回来系统保存的哈希表中SyncList结果中,进行链式查找。

st` 3 9 }at. X w E * ] ) M +ic SyncData* id2data(id object, enum usage why)
{
...
{
SyncData* p;
SyncData* firstUnused = NULL;
for (p = *listp; p != NULL; p = p->nextData) {
if ( p-&I - P _ ` k | bgt;objec] X Ct == object ) {
result = p;
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;
}
if ( (firstUnused == NULL) && (p->threadCount~ O = . v == 0) )
firstUnused = p;
}
// no SyncData currently associated with object
if ( (L F l o P o U 6 Cwhy == RELEASE) || (why == CHECK) )
goto done;
// an unused one was fou0 T 5 R %nd, use it
if ( first{ E q . t k wUnused != NULL ) {
resu( A / * I _ } Ilt = firstUn~ a { 4used;
result->object = (objc_objez t m * l 7 ( 5ct *0 A b b Z w)object;
result->threadCount = 1;
goto don& # s Z R ? H Je;
}
}
...
}

1.5 生成新数据并写入缓存

static SyncData* id2data(id object, enum usage why)k j I
{
...
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
resul4 - Ot->object = (objc_obj+ [ ( + ( y # q bect *)object;
result->threadCount = 1;
new (&a7 S + 6 c H Y ( vmp;result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;` x H x n
*listp = result;
done:
locL U 0 d w 7kp-&g- | ,t~ y N $ A g H;unlock();
if (C q S 4 & ~ arQ & ? # W Gesult) {
// Only ne2 x Hw ACQUIRE should get here.
// All RELEASE and CHq h ] ) P ( }ECK and rech J } @ B [ J Jursive ACQUIRE are 
// handled by the per-thread caches above.// 只有创建的 SyncData 才能j [ B q B m进入这里。// 所有的释放、检查和递归获取都是由上面的线程缓存处理
if (why == RELEASE) {
// Probably some thread is incorrectly exiting 
// while the object is held by another thread.
return nil;
}
if (why != ACQUIRE) _objc_fatal("id2data is bub g M : | h v Eggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache// 存入快速线程缓存
tls_set_direct(SYN0 [ )C_DATA_DIRECT_KEY, result)t Y g  ? % h 6;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache- : ` L ( a// 存入线程缓存
if (!cache) cache = fetch` h l D d +_cachL h e R # * 5e(YES);
cache->list[cache->used].data = result;
cache->list[cach& ^ C _ Z #e->used].lockCount = 1;
cache->used++3 C d : B ^;
}
}
ret? O Yurn result;
}

至此r x G N _ 2 f一个@synchronized的相关操作已经执行完成。总结来说就是底层保存了一个哈希表,其中存储了`SyncData`结构的一个链表,通过S $ } a U . | $ F线程缓存等操作,来进行增删改查,从来实现加解锁。但是操作结构复杂,步骤多,导致性能较高,而且需要注意传入的obj不能为空,否则无法进行锁操作。

2. dispatch_semaphore

相关信号量的底层原理,再上一章节已经讲过,W Z s 1 = 8 ;可以直接查– X 7 6 q T看☞iOS底层学习 – 多线程之GCD底层原理篇

3. NSLock

NU L s .SLock的使用也/ 9 ? p ] { [ Z非常的简单,只需要再需要进行加锁逻辑的前后,加上[_lock lock][_lock unlock]两行代码,就可以实现加锁的逻辑。

在寻找源码中,我们发现NSLock源码在Co ` S ;reFundation框架中,无法进行查看,所以我们看Swift版本的CoreFundation实现,来类比NSLock实现,应该也是差不多的。通过源码我们可以发现

  • NSLN % d , P lock就是对pthread_mutex互斥锁的一种上层封装。
  • 是一种互斥锁,但不是递归锁
open class NSLock: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(i? & E } * o ? @ rOS)T U _ || os(Win+ L & @ q : ydows)
private var t@ . J b . ) _imeoutCond = _ConditionVari~ G d t ^ & : c iablePointer.allocate(capacity:y F o ? 7  1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
pthread_mutex_i/ W c ~nit(mutex, nil)
#if os(macOS) || os(iOSY ! * H t = f)
pthread_cond_init(timeo| Z t p QutCoL R 6nd, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_muteH 4 7 = q s u Qx_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMute_ m # J p e ex)
pthread_co! - , 4 { :nd_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMt n ( p / ]  -utex)
#endif
}

既然NS? Q 1 * = U `Lock不是递归锁,那么他就存在着一个坑点:当我们对同一个线程,加锁两次的话,就会造成一直阻塞,就比如下面的代码,多线程调用时,会造成lock多次,从而无法向下进行。这个时候可以使用递归锁来解决。

    NSLock *testlock = [[NSLoc9 u | j O kk alloc] init];
dispatche b Z & ^ a_async(dispatch_ge2 ( ` Jt_global_queue(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[testlock lock];
if (value > 0) {
NSLog(@"cu` Q u b K  o S Srrent value = %da X F V y l",value);
// 异步递归调用
testMethod(value - 1);
}
[t? W yestlock unlox 5 =  z :ck];
};
testMethod(10);
});

3. NSRecursiveLock

将上面例子中的NSLof x ;ck换成NSRecursiveLock就是递归锁的使用了; 7 # E x & i 1,和NSLock是类似的,并且能够解决NSLock在多线程中多次加锁的问题。

首先我们还是来看一下源码实现,发现NSRG / s )ecursiveLock也是对pthread_mutex的封装,但是初始化的时候添加了PTHREAD_MUTEX_RECURSIVE递归相关的操作。

open class NSRecursiveLocky ` n: NSObject, NSLocking {
internal var mutex = _Recu2 a { , v # ! brsiveMutexPointeC o 3 d C y 0 9r.allocate(capacity: 1)
private var timeoutCC w x m f uond = _ConditionVariablePointer.alloca= 3 [te(capacity: 1)
privaA = g i B Yte var timeoutMutex = _MutexE m g U u t kPointer.allocate(capacity: 1)
withUnsafeMutable/ R P sPo% 2 !inter(to: &attrib) { attrs in
pthread_. b ? h - Z w * :mutexa9 6 L Ottr_init(attrs)
pthread_mutexattr_settype(attrs, Int32(PTHREAD_MUu o d ` 0 Z xTEX_RECURSIVE))
pthread_mutex_init(mutex, attrs)
}
pthread_conj w f ! @ ^ ! C zd_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
public override init() {
super.init()
var atu ~ ~ 9 y  Y @trL ~ x . U Z d p Xib = pthread_mutZ O fexattr_t()
pthread_cond_i & pinit(timeoutCond, nil)
pthread_mutex_init(5 G a F T f ^ 3timeoutMutex, nil)
}
deinit {
pthread_mutei a ~ ^ sx_destr k X R J ;roy(mutex)
mutex.deinitialize(count: 1)
muV U +tex.deallocate()
deallocateTimedLoQ 9 a Q S ! ~ckData(; r  u Mcond: timeoutCond, mutex: timeoutMutex)
}
open func lock ? Q X ! t() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
// Wakeup any threads waiting in lock(beo } v f tfore:)
pthread_mutex_lock(timeoutMd 1 f 6 d d u futex)
pthread_cond_broad* j O } -cast(tim/ T * f h ! . u keoutCond)
pthread_mutex_unlock(timeoutMutex)
}
open func `try`() -> Bool {
return pthread_mutex_trylock(mutex) == 0
}
open funP z q ~ 2 - -c lock(before limit: Date) -> Bool {
if pthread_mutex_trylock(mutex) == 0 {
return true
}
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMute5 _ + s 1 O b ( Ex)
}
open var name: String?
}

我们都知道,使# K 9 b ^ p 用递归的时候,最主要的是要有一个出口,否则非常容易形K l L Q L h 6成死锁。比如刚才的代码,如果进行for循环创建多线程时。这时候就是造成死锁崩溃。

因为这个V b !时候for循环造成多线程的多次创建,& l ( 4 M开辟了多条线程,但是NSRecursiveLock对象只有一个,线程之间同一个锁的对象状态是不能共享的,所以造成了线程1进行lock后,未执行到unlock时,线程2就进行了lock,所以造成了线程 1 等线程 2 解锁,线程 2 等线程 1 解锁的死锁状况。

那么这种情况下,使用哪种方案比较好呢?

这个时候使用@synchronized可以完美解决问题,因为@synchronized锁的是同一个对象,下次线程来进行锁操作时,会先从缓存中进行查找,不会进行多次锁M u Z N O,所以是安全的。

NSRecursiveLock *recursiveLock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 100; i++) {
dispatch_async= C i e _ . X (dispatch_get_global_queu@ n 5 g d 0 Ne(0, 0), ^{
static void (^testMethod)(int);
testMethod = ^(int value){
[recursiveLock lock];
if (value > 0) {
NSLog(@"current value= %d",value);
testMethod(value - 1);
}
[recursiveLock unlock];
};
te ^ BestMethod(10);
})j a B I *;
}

常用锁总结:当只是普通I P r Q ` W线程安全的时~ ] c a M 2 r候,使用 NSLock就可以解决,而需要保证递归调用线程安全的时候,使用 NSRecursiveLock,而又需要循环,外界的线程也会造成影响的时候,为了解决死锁的问题,我们可以使用@sD j X c U f xyP 9 j g ]nchrt y Gonized来解决

4. NSCondition

NSCondition是一个条件锁。

在线程间的同步中,有这样一种情况( U j B v m 7: 线程 A 需要k L 1 a 5 h i A等条件 C 成立,才能继续往下执行.现在这个条件不成立,线程 A 就阻塞等待. 而线B 8 W Y 8程 B 在执行过程g ^ a { @ & t中,使条件 C 成立了,就唤醒线程 A 继续执行。这个时候,我们可以使用条件锁来完成相关逻辑。

条件锁的底U & B 5 0 5 ; ,层实现其实就是一个互斥锁和条件变量的封装,由于未开源,我们还是先看Swift源4 : @ u ^码。

  • NSCondition是对mutexcz $ E 6 s k 5ond` R B d 3的一/ ( ` ^ ! K种封装。cond就是用于访问和操作特定类型数据的指针y 9 ; D 1 _ U
  • wait操作在没有超时时,会阻塞线程,使其进入休眠状态,需要在lock状态下使用
  • signal操作是唤醒一个正在休眠等待的线程,需要在lock状态下使用
  • broadcast唤醒所有正在等待的线程,需要在lock状态下使用
open class NSCondition: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
// 用于访问和操作特定类型数据的指针
internal var cond = _C s =onditionVariabq V e j ; D o } (lePointer.allox  J 7 y bcate(capacity: 1)
public override ir | W B w Znit() {$ R B L
pthread_mutex_init(mutex, nil)
pthre, [ x T R f ;ad_cond_init(cond, nil)
}
open func lock() {
pthread_mutex_lock(mutex)
}
open func unlock() {
pthread_mutex_unlock(mutex)
}
open func wait() {
pthread_cond_wait(P o g 8cond, mutex)
}
open func wait(untilx { G F I } limit: Date) ->E o ; y; Bool {
// 超时
gua_ t = ( +rd var timeout = tiq  meSpecFrom(date: limit) else {
return false
}
// 没有超时
return pthread_cond_timedwait(cond, mutex, &timeout) == 0
}
open func si W d b , -gnal() {
pthread_con{ [ 2 8 ud_signal(cond)
}
open func broad: $ . ] f S 3cast() {
pthread_cond_broadcast(cond) // wait  signal
}
}

对于条件锁,x u ^ v我们经常用来解决的就是生产者-消费者模式的相关问题。比} Q B O 8 V如数组中的元素,只有在大于0的情况下,才可以进行删除操作,这种情况下,可以考虑使用条件锁。

_condition = [[NSCondition allg $ / = f R , p Voc] init];
dispatch_as? 4  = F Uync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORIW h b m r r %TY_HIGH, 0), ^{
[self pro7 M W T A Z ^ cducer];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self consumer];
});
- (void)producer{
[_condition lock];
self.ticketCount = self.ticketCount + 1;
NSLog(@"生产一个 现有 count %zd",ss R 2 Q  zelf.ticketCount);
[_condition signal];
[_condition unlock];
}
- (void)consumB S D D ] v  per{
// 线程安全
[_cY o V [ 4 a }ondition lock];
✅// 使用while因为NSCondition可以给每个线程分别加锁,但加锁后不影响其他线程进入临界区。// 所以 NSf U U u _ h - vCondition使用 wait并加锁后,并不能真正保证线程的安全。// 当一个signal操作发出时,如果有两个线程都在b 7 Q + + @ ; p做消费者操作,那同时都会消耗掉资7 G * ! z t M源,于是绕过了检查。
while (self.tickD R i B =etCount == 0) {
NSLog(@"等待 count %zd",self.ticketCount);
// 保证正常流程
[_condition wait];
}
//注意消费行为,要在等待条件判断之后
self.ticketCount -= 1;
N# K T SLog(@"消费一个 还剩 count %zd ",self.ticketCount);
[_condition unlock];
}

5. NSConditionLock

NSConditionLock。我们可以通过Swift源码查看可得

  • NSConditionLoc Z } B @ okNSCondition加线程数的封装,继承NSLockin| N - o d R tg协议,也有lockunlock等方法
  • 实现了类似dispatch_semaphj Y 4 _ N J tore的效果
open class NSConditionLock : NSObject, NSLocking {
internal var _cond = NSCondition()
internal val I !  3r _value: Int
interna @ & C -al var _thread: _swift_CFThreadRef?
public conveniex X ` ~ r V r Lnce override init() {
self.init(condition: 0)
}
pus + p a I pblic inir + p E Ot(condition: Int) {
_value = condition
}
open func lock() {
let _ = lock(bef~ ` ) o +  . 6ore & I i 9 P : Se: Date.distantFuture)
}
open func unlock() {
_cond.lock()
_thread = nil
_cond.bro: , p ~adcast()
_cond.unlock()
}
open var c} F Yondition: Int {
return _value
}
open func lock(whenCondition co2 4 5ndition: Int) {
let _ = lock(whenCondition: condition, be| ~ L K o Bfore: Date.distantFuture)
}
open func `try`() -> Bool {
return lock(O - (before: Date.distantPast)
}
open func tryLock(whenCondition c4 x 3 x )ondition: Int) -> Bool {
retW f G P [ C  iurn lock(whenCondition: condition, before: Date.distantPast)
}
ope5 R V w m + d ] =n func unlock(wit_ b # Q n Q OhCondition condition:9 4 j & B N Int) {
_U O C M v X I ucond.lock()
_thr! d y S Gead = nil
_value = condiz C , o $ ; c l |tion
_cond.broadcast()
_cond.unlock()
}
open func lock(before limit: Date) -> Bool {
_cond.lock()
while _thread !/ 4 h P ^ f ) == nil {
if !_cond.wait(until: limit) {
_cond.unlock()
return false
}
}
_thread = pth,  f E ] ^read_self()
_cond.unlock()
reB $ q G Dturn true
}
open fu&  Z W 2nc lock(whenCoA 9 S 2ndition condition: Int, before limit: D( o o W Qate) -> Bool {
// 使用 NSCondition 加锁
_cond.lock()
while _thread != nil || _value != condition {
if !_cond.wait(unt X L } Xtil: limit) {
_co y 5 x m } ^ ( Nnd.unlo) N  $ n $ Vck()
return false
}
}
_thread = pthread_self()
_cond.unlock()
return true
}
open var name: String?
}

具体的用法可以参考下面的代码

// 初始化 NSConditionLock,并设置 condition 的值为 2
NSConditionLock *conditionLoct m 9k = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_glo^ Y : )bal_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// 需要等到 condition 为 1 的时候执行下面的代码
[conditionLock lockWhenCondition:1];
NSLog(@"线程U 5 Q K w R J 1");
[conditionLock unlockWithConditi}  a *on:0];
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEd C UE_PRIORITY_LOW, 0), ^{
// 因为 ck + I C G ) w ?ondition 为 2,所以2 5 N - j Z e执行下面的代码
[conditionLock lockWhenCondition:2];
NSLog(@"线程 2");
// 解锁,并将 condition 设置为 1
[conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 因为没有条件限制,所以可以直接执行下面的代码
[conditionLock lock];
NSLog(@"线程 3");
[conditionLock unlock];
});
--------------F d M ; Q------------------------------------------------------------------------------------a l O h A ( q C *---------------------
// 打印结果
线程 3
线程 2
线程 1

6. os_unfair_lock

由于OSSpinLock自旋锁的) ~ I Z q L ;bug,在iOS10之后OSSN * p q 0 y N C XpinLock被废弃,内部封装了os_unfair_lock,而os_unfair_lockK 7 f % S f 2 H x在加锁时会处于| O [ g J ` R休眠状态,而不是自旋锁的忙等状态。

总结

  • OSSpia , ) N & i snLock之所以不在安全,是因为自旋锁会在线程等待时处* – ? j s N r A于忙等状态,会造成任务优先级翻转,倒是无法执行,目前用os_{ h A junfair_lock来替代,是一个互斥锁,互斥锁不会处于忙等,不占用时间片。
  • atomic底层实现原理就是对getsetT p .; o x 1 u N U进行加锁,但是不能保证多条线程调用或者不适用getset的线程安全,且性能消耗巨大
  • 读写锁实际是⼀种特殊的⾃旋锁,只允许一个写者写入,但是可以有多个读者。可以使用并发队列+dispatch_barriI . y U $ 1 @ Q rer_async的方法,来实现一个类似的读写锁
  • @synchronized要注意传入的对象不能为nil,否则无法加锁。底层逻辑是维护了一个全局的哈希表用来存储对象和锁,会按照缓存线程->所有线程->全局哈希表的方式进行增删改查
  • NSLock是对pthread_mutex的封装,但是没有递归逻辑。对同一个线程多次lock会造成阻塞。NSM } o 1 JRecursiveLock是在NSLocd o % N g V % ,k的基础上添加= z W了递归逻辑,当只有一个递归锁对象,多线程进行锁操作时,会造成死锁,可用@synchronized解决
  • NS= i jConditionNSConditionLock是条件锁,当满足某一个条件时,才能进行操作,Z 4 i 9适用于生产者消费者模式,和信号量dispatch_sE $ W : @ J 2 yemaphore类似

参考资料

iOS 的锁

iOS 锁1 p 3 l ] p 7 Y的底层探索笔记

发表评论

提供最优质的资源集合

立即查看 了解详情