【iOS】 锁
iOS 锁
文章目录
- iOS 锁
- 前言
- 线程安全
- 锁
- 互斥锁
- pthread_mutex
- @synchronized (互斥递归锁)
- @synchronized问题:
- 小结
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- 自旋锁
- OSSpinLock(已弃用)
- atomic
- atomic修饰的属性绝对安全吗?
- os_unfair_lock
- 读写锁
- 互斥锁和自旋锁的对比
- 小结
- 使用场景
前言
笔者在之前学习了有关于GCD的内容,今天来学习一下有关于锁的知识
常见的各类锁的性能比较:
从高到底依次是:OSSpinLock(自旋锁) -> dispatch_semaphone(信号量) -> pthread_mutex(互斥锁) -> NSLock(互斥锁) -> NSCondition(条件锁) -> pthread_mutex(recursive 互斥递归锁) -> NSRecursiveLock(递归锁) -> NSConditionLock(条件锁) -> synchronized(互斥锁)
线程安全
首先认识一下为什么要线程安全?
多个线程访问统一块资源的时候,很容易引起一个数据混乱问题.比方说下面这段代码:
__block int num = 0;while (num < 5) {dispatch_async(que, ^{num++;});
}
if (num >= 5) {NSLog(@"%ld", num);
}
这里就出现了一个数据的竞态的问题,我们最后输出的结果不会和我们的预期一致,我们想要的是一个当他大于5的时候就应该推出了,但是结果和我们想得不太一样.这里的输出结果是这个:
这里为什么会出现这个问题,是因为对于num这个致的操作不是原子性的,可能会出现多个线程同时访问到这个内存,当时都是3,然后每个线程各自给他加一导致了一个 + 3的效果,从而导致了一个线程问题.
可以理解为我们同时访问一个临界区的内容:然后让数据同时增加.
这里我们就引入了一个互斥访问的概念:
任意时刻只允许至多一个线程访问,从而保证一个线程的可以安全对于共享资源进行操作,从而解决这种临界区问题
解决方法可以看这个图片:
常见的解决临界区问题,用于一个同步计数就是加锁
锁
作用
锁最为一种非强制的机制,被用阿里保证线程安全,每一个线程在访问数据或者资源钱,要先获取锁,并在访问结束之后释放锁,如果锁已经被占用,其他想要获取锁的线程会等待知道锁重新可以使用
不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了
分类
在iOS中锁的基本种类只有两种:互斥锁
、自旋锁
读写锁(其实是一种特殊的自旋锁)
,其他的比如条件锁
、递归锁
、信号量
都是上层的封装和实现
互斥锁
互斥锁防止两条线程同时对同一公共资源进行读写的机制.当获取锁操作失败的时候,线程会进入睡眠,等待锁释放时被唤醒
互斥锁
(Mutual exclusion,缩写Mutex
)防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒:
- 递归锁:可重入锁,同一个线程在锁释放前可以再次获得锁,即可以递归调用
- 非递归锁:不可重入,必须等锁释放后才可以再次获取锁
pthread_mutex
pthread_mutex
就是互斥锁
本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠.
// 导入头文件
#import <pthread.h>// 全局声明互斥锁
pthread_mutex_t _lock;// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// 解锁
pthread_mutex_unlock(&_lock);// 释放锁
pthread_mutex_destroy(&_lock);
@synchronized (互斥递归锁)
- 开启汇编调试,发现
@synchronized
在执行过程中,会走底层的objc_sync_enter
和objc_sync_exit
方法
int objc_sync_enter(id obj)
{int result = OBJC_SYNC_SUCCESS;if (obj) { // 传入不为nilSyncData* data = id2data(obj, ACQUIRE); //重点ASSERT(data);data->mutex.lock(); // 加锁} else { //传入nil// @synchronized(nil) does nothingif (DebugNilSync) {_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");}objc_sync_nil();}return result;
}int objc_sync_exit(id obj)
{int result = OBJC_SYNC_SUCCESS;if (obj) { // obj不为nilSyncData* 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 { //obj为nil时,什么也不做// @synchronized(nil) does nothing}return result;
}
通过上面两个实现逻辑的对比,发现它们有一个共同点,在obj存在时,都会通过id2data
方法,获取SyncData
这时候看一下SyncData
typedef struct alignas(CacheLineSize) SyncData {struct SyncData* nextData; // 类似于链表的结构DisguisedPtr<objc_object> object; //锁住的对象int32_t threadCount; // number of THREADS using this blockrecursive_mutex_t mutex; // 递归锁
} SyncData;
typedef struct {SyncData *data;unsigned int lockCount; // number of times THIS THREAD locked this block
} SyncCacheItem;typedef struct SyncCache {unsigned int allocated;unsigned int used;SyncCacheItem list[0]; // 用于存储线程,其中当前的线程的链表data,用于存储SyncData和lockCount
} SyncCache;
封装了recursive_mutex_t
属性,可以确认@synchronized
确实是一个递归互斥锁
,然后可以看出他是一个一个链表的形式存储的,
然后之所以一个线程会需要多个SyncData是为了给对应的一个对象加上锁,实现一个对象记别的一个管理
这时候进入id2data
方法(进入id2data
源码,从上面的分析,可以看出,这个方法是加锁和解锁
都复用的方法)
static SyncData* id2data(id object, enum usage why) //枚举值和锁住的对象传入
{spinlock_t *lockp = &LOCK_FOR_OBJ(object);SyncData **listp = &LIST_FOR_OBJ(object);SyncData* result = NULL;#if SUPPORT_DIRECT_THREAD_KEYS //(tls) 本地的局部线程缓存// Check per-thread single-entry fast cache for matching objectbool fastCacheOccupied = NO;//通过KVC方式对线程进行获取,线程绑定的dataSyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);//如果线程缓存中有data,执行if流程if (data) {fastCacheOccupied = YES;//如果在线程空间找到了dataif (data->object == object) {// Found a match in fast cache.uintptr_t lockCount;result = data;//通过KVC获取lockCount,lockCount用来记录被锁了几次,即该锁可嵌套lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);if (result->threadCount <= 0 || lockCount <= 0) {_objc_fatal("id2data fastcache is buggy");}switch(why) {case ACQUIRE: {//objc_sync_enter走这里,传入的是ACQUIRE -- 获取lockCount++;//通过lockCount判断被锁了几次,即表示 可重入tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//设置break;}case RELEASE://objc_sync_exit走这里,传入的why是RELEASE -- 释放lockCount--;tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);if (lockCount == 0) {// remove from fast cache//移除快速缓存的部分tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);// atomic because may collide with concurrent ACQUIREOSAtomicDecrement32Barrier(&result->threadCount);}break;case CHECK:// do nothingbreak;}return result;}}
#endif// Check per-thread cache of already-owned locks for matching objectSyncCache *cache = fetch_cache(NO);//判断缓存中是否有该线程//如果cache中有,方法与线程缓存一致.if (cache) {unsigned int i;for (i = 0; i < cache->used; i++) { //遍历总表SyncCacheItem *item = &cache->list[i];if (item->data->object != object) continue;// Found a match.result = item->data;if (result->threadCount <= 0 || item->lockCount <= 0) {_objc_fatal("id2data cache is buggy");}switch(why) {case ACQUIRE: //加锁item->lockCount++;break;case RELEASE: // 解锁item->lockCount--;if (item->lockCount == 0) {// remove from per-thread cache//清楚cache中清楚标识cache->list[i] = cache->list[--cache->used];// atomic because may collide with concurrent ACQUIREOSAtomicDecrement32Barrier(&result->threadCount);}break;case CHECK:// do nothingbreak;}return result;}}// Thread cache didn't find anything.// Walk in-use list looking for matching object// Spinlock prevents multiple threads from creating multiple // locks for the same new object.// We could keep the nodes in some hash table if we find that there are// more than 20 or so distinct locks active, but we don't do that now.//第一次进入,所有缓存都找不到lockp->lock();{SyncData* p;SyncData* firstUnused = NULL;for (p = *listp; p != NULL; p = p->nextData) { //cache中找到了if ( p->object == object ) { //如果不等于空,且与object相同result = p; //赋值// atomic because may collide with concurrent RELEASEOSAtomicIncrement32Barrier(&result->threadCount);goto done;}if ( (firstUnused == NULL) && (p->threadCount == 0) )firstUnused = p;}// no SyncData currently associated with object//没有与当前对象关联的SyncDataif ( (why == RELEASE) || (why == CHECK) )goto done;// an unused one was found, use it//第一次进入,没有找到if ( firstUnused != NULL ) {result = firstUnused;result->object = (objc_object *)object;result->threadCount = 1;goto done;}}// Allocate a new SyncData and add to list.// XXX allocating memory with a global lock held is bad practice,// might be worth releasing the lock, allocating, and searching again.// But since we never free these guys we won't be stuck in allocation very often.posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData)); //创建赋值result->object = (objc_object *)object;result->threadCount = 1;new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);result->nextData = *listp;*listp = result;done:lockp->unlock();if (result) {// Only new ACQUIRE should get here.// All RELEASE and CHECK and recursive ACQUIRE are // handled by the per-thread caches above.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 buggy");if (result->object != object) _objc_fatal("id2data is buggy");#if SUPPORT_DIRECT_THREAD_KEYSif (!fastCacheOccupied) { // 判断是否支持栈存缓存,支持KVC形式赋值存入tls// Save in fast thread cachetls_set_direct(SYNC_DATA_DIRECT_KEY, result);tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);} else
#endif{// Save in thread cacheif (!cache) cache = fetch_cache(YES);//第一次存储时,对线程进行了绑定cache->list[cache->used].data = result;cache->list[cache->used].lockCount = 1;cache->used++;}}return result;
}
这边先给出一个图来帮助理解:
- 首先我们可以通过这种图看出一个线程的数据是通过
哈希表
结构中通过SyncList
结构来组装多线程
的情况,这里先通过线程来对应多个链表,然后把多个对于不同对象的锁的内容组织成一个链表.SyncData
通过链表
的形式组装当前可重入
的情况,(也就是在一个线程中可以多次加锁不会出现死锁的问题). lockCount、threadCount
,解决了递归互斥锁,解决了嵌套可重入
这里简单在介绍一下相关流程:
- 现在tls中进行一个寻找
- 在
tls_get_direct
方法中以线程为key
,通过KVC
的方式获取与之绑定的SyncData
,即线程data。其中的tls
(),表示本地局部的线程缓存
- 判断data的加锁对象和我们的这次传入的对象是否一致
- 如果不一致就获取对应的一个lockCount,实现一个重入
- 通过传入的一个why来判读是加锁还是解锁
- 在
- 如果tls中没有,则在
cache缓存
中查找- 通过
fetch_cache
方法查找cache缓存中是否有线程 - 如果有,则遍历
cache总表
,读取出线程对应的SyncCacheItem
- 从
SyncCacheItem
中取出data
,然后后续步骤与tls的匹配是一致的
- 通过
- 如果cache中也没有,即
第一次进来
,则创建SyncData
,并存储到相应缓存中
第一进入没有锁:
- threadCount 和 lockCount 全部设置成1
- 存储到
tls
进入有数据,但是在同一个数据且对同一个对象加锁:
- lockCount 加1
- 存储到
tls
不是第一次进来,且是不同线程
- threadCount 加一
- 存储到tls
@synchronized问题:
for (int i = 0; i < 200000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{@synchronized (self.array) {self.array = [NSMutableArray array];}});}
这里我们给self.array
加锁,我们注意到在@synchronized
里面对于nil对象是不做任何处理的,这里会导致一个问题也就是我们会访问到野指针,因为我们每一次set方法会释放原先的内容,在retain,可能在某一个瞬间他被retain了两次导致了我们出现访问坏内存的文体:
注意:野指针 vs 过渡释放
野指针
:是指由于过渡释放产生的指针还在进行操作过渡释放
:每次都会retain 和 release
小结
@synchronized
是一个递归互斥锁@synchronized
的可重入,即可嵌套
,主要是由于lockCount
和threadCount
的搭配- 但是由于底层中
链表查询、缓存的查找以及递归
,是非常耗内存
以及性能
的,导致性能低
,所以在前文中,该锁的排名在最后 - 不能使用
非OC对象
作为加锁对象,因为其object
的参数为id
@synchronized (self)
这种适用于嵌套次数较少
的场景。
NSLock
底层是通过pthread_mutex
互斥锁实现的
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);});
这里会出现一个递归阻塞的问题:
会出现一直等待的情况,主要是因为
嵌套使用的递归
,使用NSLock(简单的互斥锁,如果没有回来,会一直睡觉等待)
,即会存在一直加lock,等不到unlock 的堵塞情况
NSRecursiveLock
NSRecursiveLock
有一个标识PTHREAD_MUTEX_RECURSIVE
,而NSLock
是默认的,这个用来解决一个递归嵌套调用的问题
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);});
这样就解决了这里的一个递归嵌套加锁的问题:
但是如果在外层加一个for循环又会出现问题:
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];for (int i = 0; i< 100; 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循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁——找不到解锁的出口
就是线程1中加锁1,线程2中加锁2.解锁1等待解锁2,解锁2等待解锁1,无法结束解锁–=形成死锁
解决: 可以采用使用缓存的@synchronized
,因为它对对象进行锁操作,会先从缓存查找是否有锁syncData
存在。如果有,直接返回而不加锁,保证锁的唯一性
NSCondition
条件锁是一种特殊类型的同步机制,用来管理不同线程间的执行顺序,让某些线程能在满足特定条件的时候才会继续
NSCondition
是一个条件锁,可能平时用的不多,但与信号量相似:线程1需要等到条件1满足才会往下走,否则就会堵塞等待,直至条件满足
NSCondition的对象实际上作为一个锁
和 一个线程检查器
- 锁是为了当检测条件的时候保护数据源,执行条件引发的任务
- 线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞
//初始化
NSCondition *condition = [[NSCondition alloc] init]//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
[condition lock];//与lock 同时使用
[condition unlock];//让当前线程处于等待状态
[condition wait];//CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];
- 它也是对于下层
pthread_mutex
的一个封装 wait
操作会阻塞线程.让他进入休眠状态,直到超时signal
操作是唤醒
一个正在休眠等待的线程broadcast
会唤醒所有正在等待的线程
NSConditionLock
NSConditionLock
是条件锁,一旦一个线程获得锁,其他线程一定等待
和上面的NSCondition
对比,NSConditonLock
使用更加简单
//初始化
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];//表示 conditionLock 期待获得锁,如果没有其他线程获得锁(不需要判断内部的 condition) 那它能执行此行以下代码,如果已经有其他线程获得锁(可能是条件锁,或者无条件 锁),则等待,直至其他线程解锁
[conditionLock lock]; //表示如果没有其他线程获得该锁,但是该锁内部的 condition不等于A条件,它依然不能获得锁,仍然等待。如果内部的condition等于A条件,并且 没有其他线程获得该锁,则进入代码区,同时设置它获得该锁,其他任何线程都将等待它代码的 完成,直至它解锁。
[conditionLock lockWhenCondition:A条件]; //表示释放锁,同时把内部的condition设置为A条件
[conditionLock unlockWithCondition:A条件]; // 表示如果被锁定(没获得 锁),并超过该时间则不再阻塞线程。但是注意:返回的值是NO,它没有改变锁的状态,这个函 数的目的在于可以实现两种状态下的处理
return = [conditionLock lockWhenCondition:A条件 beforeDate:A时间];//其中所谓的condition就是整数,内部通过整数比较条件
NSConditionLock
,其本质就是NSCondition + Lock
对于NSCondition
的一个再次的封装
NSConditionLock
可以设置锁条件
,即condition值,而NSCondition
只是信号的通知
自旋锁
自旋锁
:线程反复检查锁变量是否可⽤。由于线程在这⼀过程中保持执⾏, 因此是⼀种忙等待
。⼀旦获取了⾃旋锁,线程会⼀直保持该锁,直⾄显式释 放⾃旋锁
自旋锁避免了进程上下问的调度开销,因此对于线程只会阻塞很短事件的场合是有效的
OSSpinLock(已弃用)
自旋锁之所以不安全,是因为获取锁后,线程会一直处于忙等待
,造成了任务的优先级反转
。
其中的忙等待机制可能会造成高优先级任务一直running等待
,占用时间片,而低优先级的任务无法抢占时间片
,会造成一直不能完成,锁未释放的情况
OSSpinLock lock = OS_SPINLOCK_INIT;
OSSpinLockLock(&lock);
OSSpinLockUnlock(&lock);
因为他会出现一个优先级反转的问题,所以在后面就把它弃用了.
在OSSpinLock
被弃用后,其替代方案是内部封装了os_unfair_lock
,而os_unfair_lock
在加锁时会处于休眠状态
,而不是自旋锁的忙等状态
atomic
这里看底层的一个实现:
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);
}
- 原子性的set方法加了锁
spinlock_t
- 非原子性就是直接赋值.
这个锁其实就是os_unfair_lock
代替了OSSpinLock
using spinlock_t = mutex_tt<LOCKDEBUG>;class mutex_tt : nocopy_t {os_unfair_lock mLock;...
}
这里还进行了一个加盐操作,还采用了加盐操作来避免哈希冲突
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {if (offset == 0) {return object_getClass(self);}// Retain release worldid *slot = (id*) ((char*)self + offset);if (!atomic) return *slot;// Atomic retain release worldspinlock_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);
}
getter
方法中对atomic的处理,同setter是大致相同的
atomic修饰的属性绝对安全吗?
atomic
这个方法只可以保证我们的setter和getter方法的线程安全,并不能保证数据安全:
dispatch_async(dispatch_get_global_queue(0, 0), ^{for (int i = 0; i < 10000; i++) {self.index = self.index + 1;NSLog(@"%ld--%ld", i, self.index);}});dispatch_async(dispatch_get_global_queue(0, 0), ^{for (int i = 0; i < 10000; i++) {self.index = self.index + 1;NSLog(@"%ld--%ld", i, self.index);}});
如上图所示,这里并没有保证一个线程安全,
- atmic只可以保证变量在取值和赋值的时候的线程安全
- 不可以保证加锁是安全
os_unfair_lock
由于OSSpinLock
并不安全,因此苹果推出了os_unfair_lock
以解决优先级反转的问题
//创建一个锁os_unfair_lock_t unfairLock;
//初始化unfairLock = &(OS_UNFAIR_LOCK_INIT);//加锁os_unfair_lock_lock(unfairLock);//解锁os_unfair_lock_unlock(unfairLock);
读写锁
读写锁
实际是一种特殊的自旋锁
,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁
而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的CPU
数
-
写者是具有排他性的,一个读写锁只能有一个写者,但不能同时拥有读者和写者,在读写锁保持期间也是抢占失效的
-
如果读写锁当前没有读者也没有写者,那么写着可以立刻获得读写锁,否则他就会自旋在哪里,知道没有任何读者和写者.如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁
//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&_lock, NULL);//读加锁
pthread_rwlock_rdlock(&_lock);
//读尝试加锁
pthread_rwlock_trywrlock(&_lock)//写加锁
pthread_rwlock_wrlock(&_lock);
//写尝试加锁
pthread_rwlock_trywrlock(&_lock)//解锁
pthread_rwlock_unlock(&_lock);
//销毁
pthread_rwlock_destroy(&_lock);
现在基本上不采用,都采用栅栏函数来实现一个多读单写.
互斥锁和自旋锁的对比
前者线程会从sleep(加锁)->解锁, 这个过程中有上下文的一个切换,cpu的抢占,信号的发送和开销.
后者的线程一直都是running,死循环检查所得一个标志位,机制并不复杂
互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu。起始开销虽然低于互斥锁,但随着持锁时间编程,加锁的开销是线程增长。
小结
OSSpinLock
由于安全性问题,在iOS10之后就被废了,底层实现采用os_unfair_lock
代替
OSSpinLock
会处于忙等待os_unfair_lock
会处于休眠状态
atomic原子锁
自带一把自旋锁,只能保证setter、getter
时的线程安全,在日常开发中使用更多的还是nonatomic
修饰属性
atomic
只有当属性调用setter.getter
时的线程安全,会加上自旋锁OSSpinLock
用于保证同一个时刻只有一个线程调用属性的杜或者写nonatomic
线程不安全
@synchronized
在底层维护了一个哈希表
进行线程data的存储,通过链表
表示可重入
(即嵌套)的特性,虽然性能较低,但由于简单好用,使用频率很高
NSLock
、NSRecursiveLock
底层是对pthread_mutex
的封装
NSCondition
和NSConditionLock
是条件锁,底层都是对pthread_mutex
的封装,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore
类似
使用场景
- 如果只是简单的使用,例如涉及线程安全,使用
NSLock
- 如果是
循环嵌套
,推荐使用@synchronized
主要因为使用递归锁的性能不如使用synchronized
- 在循环嵌套中,如果对递归掌握的很好,则建议使用
递归锁
- 如果是
循环嵌套
,并且还有多线程影响
时,例如有等待、死锁现象时,建议使用@synchronized
相关文章:

【iOS】 锁
iOS 锁 文章目录 iOS 锁前言线程安全锁互斥锁pthread_mutexsynchronized (互斥递归锁)synchronized问题:小结 NSLockNSRecursiveLockNSConditionNSConditionLock 自旋锁OSSpinLock(已弃用)atomicatomic修饰的属性绝对安全吗?os_unfair_lock 读写锁互斥锁和自旋锁的对比 小结使…...

uni-app学习笔记十五-vue3页面生命周期(一)
页面生命周期概览 vue3页面生命周期如下图所示: onLoad 此时页面还未显示,没有开始进入的转场动画,页面dom还不存在。 所以这里不能直接操作dom(可以修改data,因为vue框架会等待dom准备后再更新界面)&am…...
Flink核心概念小结
文章目录 前言引言数据流API基于POJO的数据流基本源流配置示例基本流接收器数据管道与ETL(提取、转换、加载)一对一映射构建面向流映射的构建键控流进行分组运算RichFlatMapFunction对于流的状态管理连接流的使用流式分析水位的基本概念和示例侧道输入的基本概念和示例Process …...

《软件工程》第 14 章 - 持续集成
在软件工程的开发流程中,持续集成是保障代码质量与开发效率的关键环节。本章将围绕持续集成的各个方面展开详细讲解,结合 Java 代码示例与可视化图表,帮助读者深入理解并实践相关知识。 14.1 持续集成概述 14.1.1 持续集成的相关概念 持续集…...
大模型 Agent 中的通用 MCP 机制详解
1. 引言 大模型(Large Language Model,LLM)技术的迅猛发展催生了一类全新的应用范式:LLM Agent(大模型 Agent)。简单来说,Agent 是基于大模型的自治智能体,它不仅能理解和生成自然语言,还能通过调用工具与环境交互,从而自主地完成复杂任务。ChatGPT 的出现让人们看到…...
Navicat 17 SQL 预览时表名异常右键表名,点击设计表->SQL预览->另存为的SQL预览时,表名都是 Untitled。
🧑💻 用户 Navicat 17 SQL 预览时表名异常右键表名,点击设计表->SQL预览->另存为的SQL预览时,表名都是 Untitled。 🧑🔧 官方技术中心 了解到您的问题,这个显示是正常的,…...

Orpheus-TTS:AI文本转语音,免费好用的TTS系统
名人说:博观而约取,厚积而薄发。——苏轼《稼说送张琥》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、Orpheus-TTS:重新定义语音合成的标准1. 什么是Orpheus-TTSÿ…...
Python爬虫实战:研究Goose框架相关技术
一、引言 随着互联网的迅速发展,网络上的信息量呈爆炸式增长。从海量的网页中提取有价值的信息成为一项重要的技术。网络爬虫作为一种自动获取网页内容的程序,在信息收集、数据挖掘、搜索引擎等领域有着广泛的应用。本文将详细介绍如何使用 Python 的 Goose 框架构建一个完整…...
webpack优化方法
以下是Webpack优化的系统性策略,涵盖构建速度、输出体积、缓存优化等多个维度,配置示例和原理分析: 一、构建速度优化 1. 缩小文件搜索范围 module.exports {resolve: {// 明确第三方模块的路径modules: [path.resolve(node_modules)],// …...

STM32 Keil工程搭建 (手动搭建)流程 2025年5月27日07:42:09
STM32 Keil工程搭建 (手动搭建)流程 觉得麻烦跳转到最底部看总配置图 1.获取官方标准外设函数库 内部结构如下: 文件夹功能分别为 图标(用不上)库函数(重点) Libraries/ ├── CMSIS/ # ARM Cortex-M Microcontroller Software Interface Standard…...
MyBatis 框架使用与 Spring 集成时的使用
MyBatis 创建项目mybatis项目,首先需要使用maven导入mybatis库 poml.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema…...

OpenGL Chan视频学习-7 Writing a Shader inOpenGL
bilibili视频链接: 【最好的OpenGL教程之一】https://www.bilibili.com/video/BV1MJ411u7Bc?p5&vd_source44b77bde056381262ee55e448b9b1973 函数网站: docs.gl 说明: 1.之后就不再整理具体函数了,网站直接翻译会更直观也会…...

顶会新方向:卡尔曼滤波+目标检测
卡尔曼虑波+目标检测创新结合,新作准确率突破100%! 一个有前景且好发论文的方向:卡尔曼滤波+目标检测! 这种创新结合,得到学术界的广泛认可,多篇成果陆续登上顶会顶刊。例如无人机竞速系统 Swift,登上nat…...
数据库相关问题
1.保留字 1.1错误案例(2025/5/27) 报错: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near condition, sell…...

一起学数据结构和算法(二)| 数组(线性结构)
数组(Array) 数组是最基础的数据结构,在内存中连续存储,支持随机访问。适用于需要频繁按索引访问元素的场景。 简介 数组是一种线性结构,将相同类型的元素存储在连续的内存空间中。每个元素通过其索引值(数…...

Linux基本指令篇 —— touch指令
touch是Linux和Unix系统中一个非常基础但实用的命令,主要用于操作文件的时间戳和创建空文件。下面我将详细介绍这个命令的用法和功能。 目录 一、基本功能 1. 创建空文件 2. 同时创建多个文件 3. 创建带有空格的文件名(需要使用引号) 二、…...

【后端高阶面经:消息队列篇】23、Kafka延迟消息:实现高并发场景下的延迟任务处理
一、延迟消息的核心价值与Kafka的局限性 在分布式系统中,延迟消息是实现异步延迟任务的核心能力,广泛应用于订单超时取消、库存自动释放、消息重试等场景。 然而,Apache Kafka作为高吞吐的分布式消息队列,原生并不支持延迟消息功能,需通过业务层或中间层逻辑实现。 1.1…...

Mac安装MongoDB数据库以及MongoDB Compass可视化连接工具
目录 一、安装 MongoDB 社区版 1、下载 MongoDB 2、配置环境变量 3、配置数据和日志目录 4、启动MongoDB服务 5、使用配置文件启动 6、验证服务运行 二、MongoDB可视化工具MongoDB Compass 一、安装 MongoDB 社区版 1、下载 MongoDB 大家可以直接在官方文档下安装Mo…...

城市地下“隐形卫士”:激光甲烷传感器如何保障燃气安全?
城市“生命线”面临的安全挑战 城市地下管网如同人体的“血管”和“神经”,承载着燃气、供水、电力、通信等重要功能,一旦发生泄漏或爆炸,将严重影响城市运行和居民安全。然而,由于管线老化、违规施工、监管困难等问题࿰…...

MySQL推出全新Hypergraph优化器,正式进军OLAP领域!
在刚刚过去的 MySQL Summit 2025 大会上,Oracle 发布了一个用于 MySQL 的全新 Hypergraph(超图)优化器,能够为复杂的多表查询生成更好的执行计划,从而优化查询性能。 这个功能目前只在 MySQL HeatWave 云数据库中提供&…...

飞牛fnNAS手机相册备份及AI搜图
目录 一、相册安装应用 二、手机开启自动备份 三、开始备份 四、照片检索 五、AI搜图设置 六、AI搜图测试 七、照片传递 现代的手机,已经成为我们最亲密的“伙伴”。自从手机拍照性能提升后,手机已经完全取代了简单的卡片相机,而且与入门级“单反”相机发起了挑战。在…...

消费类,小家电产品如何做Type-C PD快充快速充电
随着快充技术的快速发展现在市场上的产品接口都在逐渐转为Type-C接口,Type-C可以支持最大20V100W的功率。未来Type-C大概会变成最通用的接口,而你的产品却还是还在用其他的接口必然会被淘汰, 而要使小家电用到PD快充,就需要使用到Type-C快充诱…...

连接表、视图和存储过程
1. 视图 1.1. 视图的概念 视图(View):虚拟表,本身不存储数据,而是封装了一个 SQL 查询的结果集。 用途: 只显示部分数据,提高数据访问的安全性。简化复杂查询,提高复用性和可维护…...
人工智能赋能教育:重塑学习生态,开启智慧未来
在科技浪潮风起云涌的当下,人工智能(AI)如同一颗璀璨的新星,正以前所未有的速度和深度融入社会生活的各个领域。教育,作为塑造未来、传承文明的核心领域,自然也未能置身事外。人工智能与教育的结合…...

银河麒麟V10×R²AIN SUITE:用AI重构安全,以国产化生态定义智能未来
前言 银河麒麟是由国防科技大学研发、现由麒麟软件运营的国产操作系统,旨在打破国外技术垄断,保障国家信息安全。自2002年国家“863计划”启动以来,历经技术迭代与生态整合,现为国产操作系统领军品牌。其应用覆盖党政、国防、能源…...

JavaScript- 3.2 JavaScript实现不同显示器尺寸的响应式主题和页面
本系列可作为前端学习系列的笔记,代码的运行环境是在HBuilder中,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。 HTML和CSS系列文章 已经收录在前端专栏,有需要的宝宝们可以点击前端专栏查看ÿ…...

15.进程间通信(一)
一、进程间通信介绍 进程间通信目的: 数据传输:一个进程需要将它的数据发送给另⼀个进程 资源共享:多个进程之间共享同样的资源。 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们…...

AI 数据采集实战指南:基于 Bright Data 快速获取招标讯息
AI 数据采集实战指南:基于Bright Data快速获取招标讯息 在招标行业中,快速、准确地获取招标公告、项目详情、投标截止日期和其他关键招标信息,是投标企业提高竞标成功率的核心竞争力。然而,招标信息往往分散在不同的平台和网页&a…...

cursor使用mcp
问题说明 mcp就相当于给AI安装了工具包,它可以调用获取接口文档,网页,数据库等,基本上所有的mcp都是node程序,少数需要python环境 使用说明 使用mcp-mysql举例,下面是配置json "mysql": {&qu…...

小白成长之路-计算机网络(四)
文章目录 前言一、网络连接查看1.netstat2.ss3.bond绑定3.1准备好这三个文件3.2添加bond配置文件3.3关闭网络图形化服务3.4重启 4.Linux下的抓包工具Wireshark 5、web压力测试工具6、路由追踪命令 二、[练习题](https://blog.csdn.net/m0_70730767/article/details/148262716?…...