iOS ------ 锁

一,线程安全

简单来说就是,在同一个时刻对同一个数据的操作只有一个。而线程不安全,则是在同一个时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。

二,锁的作用

锁作为一种非强制的机制,被用来保证线程安全。每个线程在访问数据时,需要先获取(Acquire)锁,并在结束的时候释放(release)锁。如果锁已经被占用,其他获取锁的线程会等待,直到锁从新可用。

三,锁的分类

主要分为两大类:互斥锁自旋锁条件锁递归锁信号量都是上层的分装盒实现。

互斥锁:

互斥锁防止两条线程同时对同一公共资源进行读写的机制。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。

互斥锁又分为递归锁非递归锁

递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用
非递归锁:不可重入,必须等锁释放后才能再次获取锁

互斥锁:@synchronized,NSLock, pthread_mutex, NSConditionLock, NSCondition, NSRecursiveLock

自旋锁

线程反复确认锁变量是否可用。由于线程在这一过程保持可执行,因此是一种忙等待。一旦获取到自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

两者的区别

  • 自旋锁的优点在于,自旋锁不会引起调用者线程的休眠,所以不会进行线程调度,CPU的时间片轮换的耗时操作。如果在短时间内获得锁,自旋锁的效率高于互斥锁。
  • 自旋锁的不足在于,自旋锁一直占用CPU资源 ,在为获得锁的情况下,会一直自旋,相当于死循环。如果不能在短时间内获得锁,会是CPU的效率降低。自旋锁不能实现递归调用。
    自旋锁:atomicOSSpinLockdispatch_semaphore_t

自旋锁

OSSpinLock

自从OSSpinLock出现了安全问题之后就废弃了。自旋锁之所以不安全,是因为自旋锁由于获取锁时,线程会一直处于忙等待状态,造成了任务的优先级反转
而OSSpinLock忙等的机制就可能造成高优先级一直running等待,占用CPU时间片;而低优先级任务无法抢占时间片,变成迟迟完不成,不释放锁的情况.

atomic

atomic原理

自动生成的setter方法胡根据修饰符的不同调用不同的方法,最后统一调用在这里插入代码片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);
}
  • 原子性修饰的属性进行了spinLock加锁处理
  • 非原子性的除了没有加锁,其他逻辑与atomic相同

注意:OSSpinLock因为安全问题被废弃了,实际上用os_unfair_lock替代了OSSpinLock。

getter方法也是如此,atomic修饰的属性进行加锁处理

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只能保证setter和getter方法的线程安全,不能保证数据安全。

  • atomic可以保证变量在取值和赋值的时候是线程安全的
  • self.index + 1不能保证是安全的,当两个线程同时进行这一操作的时候,在存入,只能起到加1的效果
  • self.index = I是能保证setter方法的线程安全的

读写锁

读写锁是一种特殊的自旋锁,对共享资源的访问化为读者和写着。相对于自旋锁而言,能够提高并发性。它允许同时又多个读者来访问共享资源,最大可能的读者我去诶实际的CPU数。

  • 写者是排它性,一个读写锁同时只有一个写者或多个读者,不能同时既有读者又有写者。
  • 如果读写锁当前没有读者,也没有写者,那么写者可以⽴刻获得读写锁,否则它必须⾃旋在那⾥,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以⽴即获得该读写锁,否则读者必须⾃旋在那⾥,直到写者释放该读写锁。
// 导入头文件
#import <pthread.h>
// 全局声明读写锁
pthread_rwlock_t lock;
// 初始化读写锁
pthread_rwlock_init(&lock, NULL);
// 读操作-加锁
pthread_rwlock_rdlock(&lock);
// 读操作-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写操作-加锁
pthread_rwlock_wrlock(&lock);
// 写操作-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 释放锁
pthread_rwlock_destroy(&lock);

互斥锁

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可能是日常开发中用的比较多的一种互斥锁,使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低。

@synchronized (obj) {}

源码:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
        {
            id _rethrow = 0;
            id _sync_obj = (id)appDelegateClassName;
            objc_sync_enter(_sync_obj);
            try {
                struct _SYNC_EXIT {
                    _SYNC_EXIT(id arg) : sync_exit(arg) {}
                    ~_SYNC_EXIT() {
                        objc_sync_exit(sync_exit);
                    }
                    id sync_exit;
                }
                _sync_exit(_sync_obj);
            }
            catch (id e) {_rethrow = e;}
            {
                struct _FIN { _FIN(id reth) : rethrow(reth) {}
                    ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
                    id rethrow;
                }_fin_force_rethow(_rethrow);
            }
        }
    }
    return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

@synchronized就是实现了objc_sync_enterobjc_sync_exit两个方法

在objc源码中找到objc_sync_enterobjc_sync_exit

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

1.如果obj存在,则通过id2data方法获取相应的syncData,对threadCountlockCount进行递增操作。
2.如果obj不存在,则调用objc_sync_nil,通过符号断点得知,这个方法里面是什么都没有做,直接return。

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

1.如果obj存在,则调用id2data方法获取对应的SyncData,对threadCountlockCount进行递减操作。
2.如果obj为nil,什么也不做,直接return

  • 如果锁的对象obj不存在时分别会走objc_sync_nil()和不做任何操作(源码分析可以先解决简单的逻辑分支)
BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);

这也是@synchronized作为递归锁但能防止死锁的原因所在:在不断递归的过程中如果对象不存在了就会停止递归从而防止死锁。

  • 正常情况下,obj存在,会通过id2data方法生成一个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;
  • nextData:链表中下一个SyncData
  • object: 当前加锁的对象
  • threadCount: 使用该对象进行加锁的线程数
  • mutex:递归锁

SyncData是通过id2data获得,而且加锁和解锁都是复用该方法
在这里插入图片描述

会现在tls中再在cache中查找线程
tlscache 表结构分析

在这里插入图片描述

  • tls哈希表结构中通过SyncList结构来组装多线程的情况
  • SyncData通过链表的形式组装,记录当前可重入的情况
  • 下层主要通过tls线程缓存,cache缓存来进行处理
  • 底层主要有两个东西:lockCountthreadCount,解决递归互斥锁,解决嵌套重入

总结:

  • @synchronized在底层封装的是一把递归锁,所以这个锁是递归互斥锁
  • @synchronized的可重入,即可嵌套,主要是由于lockCountthreadCount的搭配
  • @synchronized使用链表的原因是链表方便下一个data的插入
  • 不能使用非OC对象作为加锁条件,id2data中接收参数为id类型
  • 多次锁同一个对象会有什么后果吗,会从高速缓存中拿到data,所以只会锁一次对象
  • @synchronized性能低,因为在底层增删改查消耗了大量性能
  • 加锁对象不能为nil,否则加锁无效,不能保证线程安全

NSLock

NSLock是对互斥锁 pthread_mutex的简单封装。

- (void)test {
    self.testArray = [NSMutableArray array];
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            self.testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
}

NSLock非递归锁在递归调用时,会造成堵塞,并非死锁。第一次加锁之后还没有出锁就进行递归调用,第二次加锁就堵塞了线程(因为不能查询缓存)

- (void)test {
    NSLock *lock = [[NSLock alloc] init];
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^block)(int);
        block = ^(int value) {
            NSLog(@"加锁前");
            [lock lock];
            NSLog(@"加锁后");
            if (value > 0) {
                NSLog(@"value——%d", value);
                block(value - 1);
            }
            [lock unlock];
        };
        block(10);
    });
}

打印结果:

2024-07-31 11:17:59.586099+0800 死锁[94888:3635993] 加锁前
2024-07-31 11:17:59.586152+0800 死锁[94888:3635993] 加锁后
2024-07-31 11:17:59.586190+0800 死锁[94888:3635993] value --- 10
2024-07-31 11:17:59.586243+0800 死锁[94888:3635993] 加锁前

解决方案:

  • 移动锁的位置
NSLock* lock = [[NSLock alloc] init];
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^block)(int);
            [lock lock];
            block = ^(int value) {
                
                if(value > 0) {
                    NSLog(@"value --- %d", value);
                    block(value  - 1);
                }
               
            };
            block(10);
            [lock unlock];
        });

输出结果为依次从10打印到1

  • 使用@synchronized
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^block)(int);
            block = ^(int value) {
                @synchronized (self) {
                    if(value > 0) {
                        NSLog(@"value --- %d", value);
                        block(value  - 1);
                    }
                }
            };
            block(10);
        });

输出结果为依次从10打印到1

  • 使用递归锁NSRecursiveLock替换NSLock
NSRecursiveLock* lock = [[NSRecursiveLock alloc] init];
        //for(int i = 0; i < 10; i++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                static void (^block)(int);
                block = ^(int value) {
                    [lock lock];
                    if(value > 0) {
                        NSLog(@"value --- %d", value);
                        block(value  - 1);
                    }
                    [lock unlock];
                };
                block(10); 
            });
        //}

只输出一边从10到1。

注意这里加入循环会造成崩溃,下面会讲

总结:

  • 在同一线程调用NSLock的两次lock方法将永久锁定线程
  • 向NSLock对象发送解锁消息时,必须确保该消息是从发送初始锁定消息的同一线程发送的。

NSRecursiveLcok

使用:

 NSRecursiveLock* lock = [[NSRecursiveLock alloc] init];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^block)(int);
        block = ^(int value) {
            NSLog(@"加锁前");
            [lock lock];
            NSLog(@"加锁后");
            if(value > 0) {
                NSLog(@"value --- %d", value);
                block(value  - 1);
            }
            [lock unlock];
        };
        block(10);
    });

递归锁会出现死锁 ------ 前后代码相互等待便会产生死锁。

在上述代码加上for循环。就会崩溃

NSRecursiveLock* lock = [[NSRecursiveLock alloc] init];
    for(int i = 0; i < 10; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^block)(int);
            block = ^(int value) {
                [lock lock];
                NSLog(@"加锁后");
                if(value > 0) {
                    NSLog(@"value --- %d", value);
                    block(value  - 1);
                }
                [lock unlock];
            };
            block(10);
        });
    }

原因: for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁——找不到解锁的出口
即 线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁
解决: 可以采用使用缓存的@synchronized,因为它对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性

for(int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
            static void (^block)(int);
            block = ^(int value) {
                @synchronized (self) {
                    if(value > 0) {
                        NSLog(@"value --- %d", value);
                        block(value  - 1);
                    }
                }
            };
            block(10);
        });
}

NSCondition

NSCondition是一个条件锁,可能平时用的不多,但与信号量相似:线程1需要等到条件1满足才会往下走,否则就会堵塞等待,直至条件满足。

NSCondition的对象实际上作为一个锁和一个线程检查器。

1.锁主要为了当检测条件时保护数据源,执行条件引发的任务
2.线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞

用法:

//初始化
NSCondition *condition = [[NSCondition alloc] init]

//一般用于多线程同时访问、修改同一个数据源,保证在同一 时间内数据源只被访问、修改一次,其他线程的命令需要在lock 外等待,只到 unlock ,才可访问
[condition lock];

//与lock 同时使用
[condition unlock];

//让当前线程处于等待状态
[condition wait];

//CPU发信号告诉线程不用在等待,可以继续执行
[condition signal];

1.NSCondition是对mutex和cond的一种封装(cond就是用于访问和操作特定类型数据的指针)
2.wait操作会阻塞线程,使其进入休眠状态,直至超时
3.signal操作是唤醒一个正在休眠等待的线程
4.broadcast会唤醒所有正在等待的线程

NSConditionLock

NSConditionLock定义:条件锁,一旦一个线程获得锁,其他线程一定等待。其本质就是NSCondition + Lock。

相比NSConditionLock而言,NSCondition使用比较繁琐,所以推荐使用NSConditionLock,

//初始化
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时间];

NSConditionLockNSCondition加线程的封装

NSConditionLock可以设置锁条件,而NSCondition只是通知信号

总结:

  • OSSpinLock不再安全,底层用os_unfair_lock代替
  • atomic只能保证settergetter时线程安全,所以更多使用nonatomic来修饰
  • 读写锁更多用栅栏函数来实现
  • @synchronized底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁
  • NSLock,NSRecursiveLock,NSCcondition和NSCconditionLock底层都是对pthread_mutex的封装
  • NSConditionNSCondiyionLock是条件锁,当满足了某一个条件时才能进行操作和型号量dispatch_semaphore类似
  • 普通场景下涉及到线程安全,可以用NSLock
  • 循环调用时用NSRecursiveLock
  • 循环调用且有线程影响时,请注意死锁,如果有死锁问题就使用@synchronized
  • 30
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值