【iOS】 锁

发布于:2025-05-28 ⋅ 阅读:(16) ⋅ 点赞:(0)

iOS 锁

前言

笔者在之前学习了有关于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的时候就应该推出了,但是结果和我们想得不太一样.这里的输出结果是这个:

image-20250524155857550

这里为什么会出现这个问题,是因为对于num这个致的操作不是原子性的,可能会出现多个线程同时访问到这个内存,当时都是3,然后每个线程各自给他加一导致了一个 + 3的效果,从而导致了一个线程问题.

可以理解为我们同时访问一个临界区的内容:然后让数据同时增加.

这里我们就引入了一个互斥访问的概念:

任意时刻只允许至多一个线程访问,从而保证一个线程的可以安全对于共享资源进行操作,从而解决这种临界区问题

解决方法可以看这个图片:

image-20250524160516577

常见的解决临界区问题,用于一个同步计数就是加锁

作用

锁最为一种非强制的机制,被用阿里保证线程安全,每一个线程在访问数据或者资源钱,要先获取锁,并在访问结束之后释放锁,如果锁已经被占用,其他想要获取锁的线程会等待知道锁重新可以使用

不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了

分类

在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_enterobjc_sync_exit方法
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) { // 传入不为nil
        SyncData* data = id2data(obj, ACQUIRE); //重点
        ASSERT(data);
        data->mutex.lock(); // 加锁
    } else { //传入nil
        // @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;
}

int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) { // obj不为nil
        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 { //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 block
    recursive_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 object
    bool fastCacheOccupied = NO;
  //通过KVC方式对线程进行获取,线程绑定的data
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
  //如果线程缓存中有data,执行if流程
    if (data) {
        fastCacheOccupied = YES;
				//如果在线程空间找到了data
        if (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 ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *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 ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            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 RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
      //没有与当前对象关联的SyncData
        if ( (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_KEYS
        if (!fastCacheOccupied) { // 判断是否支持栈存缓存,支持KVC形式赋值存入tls
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
#endif
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);//第一次存储时,对线程进行了绑定
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

这边先给出一个图来帮助理解:

tls和cache缓存结构

  • 首先我们可以通过这种图看出一个线程的数据是通过哈希表结构中通过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];
                }
            });
        }

image-20250525004045750

这里我们给self.array加锁,我们注意到在@synchronized里面对于nil对象是不做任何处理的,这里会导致一个问题也就是我们会访问到野指针,因为我们每一次set方法会释放原先的内容,在retain,可能在某一个瞬间他被retain了两次导致了我们出现访问坏内存的文体:

注意:野指针 vs 过渡释放

  • 野指针:是指由于过渡释放产生的指针还在进行操作
  • 过渡释放:每次都会retain 和 release
小结
  • @synchronized是一个递归互斥锁
  • @synchronized的可重入,即可嵌套,主要是由于lockCountthreadCount的搭配
  • 但是由于底层中链表查询、缓存的查找以及递归,是非常耗内存以及性能的,导致性能低,所以在前文中,该锁的排名在最后
  • 不能使用非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 的堵塞情况

image-20250525131704332

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);
        
    });

这样就解决了这里的一个递归嵌套加锁的问题:

image-20250525154234578

但是如果在外层加一个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);
            
        });
    }

image-20250525205930118

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 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);
}

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);
        }
    });

image-20250524220147128

如上图所示,这里并没有保证一个线程安全,

  • 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的存储,通过链表表示可重入(即嵌套)的特性,虽然性能较低,但由于简单好用,使用频率很高

NSLockNSRecursiveLock底层是对pthread_mutex的封装

NSConditionNSConditionLock是条件锁,底层都是对pthread_mutex的封装,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似

使用场景

  • 如果只是简单的使用,例如涉及线程安全,使用NSLock
  • 如果是循环嵌套,推荐使用@synchronized主要因为使用递归锁的性能不如使用synchronized
  • 在循环嵌套中,如果对递归掌握的很好,则建议使用递归锁
  • 如果是循环嵌套,并且还有多线程影响时,例如有等待、死锁现象时,建议使用@synchronized

网站公告

今日签到

点亮在社区的每一天
去签到