在程序开发过程中,经常使用到线程,多线程操作数据时。难免发生一些不可控的情况,造成数据不安全。这个时候我们就用了锁,常用的锁有哪些?有os_unfair_lock、NSLock、NSCondition、NSRecursiveLock等。
一. 线程锁
1、原子属性和锁
通过一个小例子看下无锁情况下的demo:
@property (nonatomic ,assign) int count;
- (void)test {
for (int i = 0; i < 10; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.count --;
NSLog(@"%d",self.count);
});
}
}
复制代码
无锁的情况下打印是无序的。哪我们把 (atomic ,assign) int count;改成原子属性呢? 看到打印结果也是无序的。atomic 原子属性也无法保证数据安全。
哪我们进入objc源码看看:
set方法里没有锁的操作 get方法里有锁的操作看到源码有加锁的操作,哪为什么数据还是不安全呢。self.count --;这个操作即有get操作,也有set操作。但是锁只在get中,所以我们打印的结果并不是有序的。 上图是各种锁的加锁10000次的性能。
锁分:互斥锁(闲等),自旋锁(忙等),读写锁3大类。
ios中有:OSSpinLock、dispatch_semaphore、pthread_mutex、NSCondition、NSLock、pthread_mutex(recursive)、NSRecursiveLock、NSConditionLock、@synchronized9把锁。
2、OSSpinLock,os_unfair_lock锁
OSSpinLock:(它有一个bug,线程优先级反转的问题:优先级低的线程资先执行,然后其他优先级高的在执行会一直占用CPU。优先级底底那个线程会资源无法释放。)
iOS10之后被移除。OS_SPINLOCK_INIT 初始化锁 。OSSpinLockLock(&spinlock) 加锁,参数为OSSPINLOCK地址。OSSpinLockUnlock(&spinlock) 解锁,参数是OSSpinLock地址。OSSpinLockTry(&spinlock) 尝试上锁,参数是OSSpinLock地址。如果返回false,表示上锁失败,锁正在被其他线程持有。如果返回true,表示上锁成功。
os_unfair_lock:
iOS10之后开始支持,用于取代OSSpinLock。OS_UNFAIR_LOCK_INIT 初始化锁。os_unfair_lock_lock 加锁。参数为os_unfair_lock地址。os_unfair_lock_unlock 解锁。参数为os_unfair_lock地址。os_unfair_lock_trylock 尝试加锁。参数为os_unfair_lock地址。如果成功返回true。如果锁已经被锁定则返回false。os_unfair_lock_assert_owner 参数为os_unfair_lock地址。如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃。os_unfair_lock_assert_not_owner 参数为os_unfair_lock地址。如果当前线程持有指定的锁,则触发崩溃。
os_unfair_lock例子:
@property (nonatomic ,assign) os_unfair_lock unfairLock;
self.unfairLock = OS_UNFAIR_LOCK_INIT;
-(void)touchesBegan:(NSSet
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self unfairLock_test];
});
}
}
-(void)unfairLock_test {
os_unfair_lock_lock(&_unfairLock);//加锁
self.count --;
NSLog(@"%d",self.count);
os_unfair_lock_unlock(&_unfairLock);//解锁
}
复制代码
打印结果是有序的。注意:os_unfair_lock_assert_owner如果当前线程未持有指定的锁或者锁已经被解锁,则触发崩溃。和os_unfair_lock_assert_not_owner如果当前线程持有指定的锁,则触发崩溃。
3、NSCondition锁
NSLock:
(void)lock 加锁。(void)unlock 解锁。(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。@property (nullable ,copy) NSString *name 锁名称。
先通过一个例子:
self.iLock = [[NSLock alloc] init];
-(void)touchesBegan:(NSSet
for (int i = 0; i<10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self nslock_test];
});
}
}
-(void)nslock_test {
[self.iLock lock];
self.count --;
NSLog(@"%d",self.count);
[self.iLock unlock];
}
复制代码
打印结果是我们想要的顺序。
如果我们重复调用[self.iLock lock];
NsLock不能循环调用,要不然会死锁。NsLock不支持递归锁。
NSCondition:
(void)lock 加锁。(void)unlock 解锁。(void)wait 阻塞当前线程,使线程进入休眠,等待唤醒信号。调用前必须已加锁。(void)waitUntilDate 阻塞当前线程,使线程进入休眠,等待唤醒信号或者超时。调用前必须已加锁。(void)signal 唤醒一个正在休眠的线程,如果要唤醒多个,需要调用多次。如果没有线程在等待,则什么也不做。调用前必须已加锁。(void)broadcast 唤醒所有在等待的线程。如果没有线程在等待,则什么也不做。调用前必须已加锁。@property (nullable ,copy) NSString *name 锁名称。
我们通过NSCondition 例子进一步了解NSCondition锁:
- (void)nscondition_test {
for (int i = 0; i < 50; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self ny_production];
});
}
for (int i = 0; i < 100; i ++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self ny_consumption];
});
}
}
- (void)ny_production {
[self.iCondition lock];
self.count ++;
NSLog(@"生产了一个产品,现有产品 : %d个",self.count);
[self.iCondition signal];//唤醒休眠线程
[self.iCondition unlock];
}
- (void)ny_consumption {
[self.iCondition lock];
while (self.count == 0) {//防止虚假唤醒
[self.iCondition wait];
}
self.count --;
NSLog(@"消费了一个产品,现有产品: %d个",self.count);
[self.iCondition unlock];
}
复制代码
一个线程for 50 次生产产品,一个线程 for 100次消费产品。在ny_production 中生产,在 ny_consumption 中消费,如果产品数量count==0 就等待(忙等)。然后消费完解锁。
4、NSRecursiveLock锁
NSConditionLock:
(void)lock 加锁。(void)unlock 解锁。(instancetype)initWithCondition:(NSinteger)初始化一个。NSConditionLock对象。@property(readonly) NSInteger condition 锁的条件。(void)lockWhenCondition:(NSInteger)conditio满足条件时加锁。(BOOL)tryLock尝试加锁。(BOOL)tryLockWhenCondition如果接受对象的condition与给定的condition相等,则尝试获取锁,不足塞线程。(void)unlockWithCondition:(NSInteger)condition解锁,重置锁的条件。(BOOL)lockBeforDate:(NSDate *)limit在指定时间点之前获取锁。(BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit在指定的时间前获取锁。@property (nullable ,copy) NSString *name 锁名称。
先通过一个例子来了解NSConditionLock:
- (void)ny_testConditonLock{
self.iConditionLock = [[NSConditionLock alloc] initWithCondition:3];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:3];//只有3==init3时才会打印NSlog
NSLog(@"线程 1");
[self.iConditionLock unlockWithCondition:2];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:2];//只有2才会打印NSlog
NSLog(@"线程 2");
[self.iConditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self.iConditionLock lockWhenCondition:1];//只有1才会打印NSlog
NSLog(@"线程 3");
[self.iConditionLock unlockWithCondition:0];
});
}
复制代码
ConditionLock可以控制线程调用顺序的作用.
NSRecursiveLock:
(void)lock 加锁。(void)unlock 解锁。(BOOL)tryLock 尝试加锁。成功返回YES,失败返回NO。(BOOL)lockBeforeDete:(NSDate *)limit 在指定时间点之前获取锁,能够获取返回YES,获取不到返回NO。@property (nullable ,copy) NSString *name 锁名称。
写一个例子来了解NSRecursiveLock:
- (void)recursiveTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static void (^recursiveMethod)(int);
recursiveMethod = ^(int value){
if (value > 0) {
[self.iRecursiveLock lock];
NSLog(@"%d",value);
recursiveMethod(value - 1);
[self.iRecursiveLock unlock];
}
};
recursiveMethod(10);
});
}
复制代码
要注意NSRecursiveLock 只能在同一线程下递归调用。
从源码中可以看出,pthread的类型被设置成了RECURSIVE。
5、NSRecursiveLock锁
pthread_mutex:
pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr)初始化锁,pthread_mutexattr_t可用来设置锁的类型。pthread_mutex_lock(pthread_mutex_t mutex);//加锁pthread_mutex_trylock(*pthread_mutex_t *mutex);//加锁,但是上面方法不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待,成功返回0.失败返回错误信息pthread_mutex_unlock(pthread_mutex_t *mutex);//释放锁pthread_mutex_destroy(pthread_mutex_t* mutex);//使用完锁之后释放锁pthread_mutexattr_setpshared();//设置互斥锁的范围pthread_mutexattr_getpshared() //获取互斥锁的范围
直接上代码:
- (void)ny_pthread_mutex {
//非递归
pthread_mutex_t lock0;
pthread_mutex_init(&lock0, NULL);
pthread_mutex_lock(&lock0);
pthread_mutex_unlock(&lock0);
pthread_mutex_destroy(&lock0);
//递归
pthread_mutex_t lock;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &attr);
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
}
复制代码
大部分锁都是基于pthread的封装。
dispatch_semaphore_t:
dispatch_semaphore_create(intptr_t value)创建信号量,并且创建的时候需要指定信号量的大小dispatch_semaphore_wait(dispatch_semaphore_t dsema, diapatch_time_t timeout) 等待信号量,如果信号量值为0,那么该函数就会一直等待(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。dispatch_semaphore_signal(dispatch_semaphore_t dsema) 发送信号量,该函数会对信号量的值进行加1操作。
写一个小例子:
- (void)lg_dispatch_semaphore_t {
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务1");
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务2");
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"任务3");
});
}
**2022-06-11 17:38:12.701495+0800 iOSLockDemo[4742:162810] 任务1**
**2022-06-11 17:38:12.702674+0800 iOSLockDemo[4742:162810] 任务2**
**2022-06-11 17:38:12.703386+0800 iOSLockDemo[4742:162810] 任务3**
复制代码
上一章我们已经了解过信号量了,这里不在过多阐述了。
6、读写锁
在开发过程中,我们经常需要达成多读,单写的操作。如何达成这一目的呢?
通过一个例子(来理解):
self.iQueue = dispatch_queue_create("ny", DISPATCH_QUEUE_CONCURRENT);//并发队列
- (NSString *)ny_read {
// 异步读取
__block NSString *ret;
dispatch_sync(self.iQueue, ^{
// 读取的代码
ret = self.dataDic[@"name"];
});
NSLog(@"%@",ret);
return ret;
}
- (void)ny_write: (NSString *)name {
// 写操作
dispatch_barrier_async(self.iQueue, ^{//栅栏
[self.dataDic setObject:name forKey:@"name"];
});
}
复制代码
分析:通过一个并发队列iQueue,写入时用栅栏函数dispatch_barrier_async写入数据,这个时候同队列后面添加的任务就会等待栅栏函数dispatch_barrier_async的任务完成在执行。如果没有写入任务,则读取的任务线程是并发的。 dispatch_sync的ny_read 外部用async来保证多读。(如下图:)
这是打印结果。
总结: 什么是线程安全?
多线程操作共享数据的时候不会出现意想不到的结果就叫线程安全,否则,就是线程不安全。原⼦属性是线程安全的吗?
原⼦属性只能保障set 或者 get的读写安全,但我们在使⽤属性的时候,往往既有set⼜有get,所以说原⼦属性并不是线程安全的。
⾃旋锁和互斥锁的区别
⾃旋锁: 在访问被锁的资源的时候,调⽤者线程不会休眠,⽽是不停循环在那⾥,直到被锁资源释放锁。(忙等)
互斥锁: 在访问被锁资源时,调⽤者线程会休眠,此时cpu可以调度其他线程⼯作。直到被锁的资 源释放锁。然后再唤醒休眠线程。(闲等)
⾃旋锁的优点在于,因为⾃旋锁不会引起调⽤者线程休眠,所以不会进⾏线程调度,cpu时间⽚轮转等⼀些耗时的操作。所以如果能在很短的时间内获得锁,⾃旋锁的效率远⾼于互斥锁。
⾃旋锁缺点在于,⾃旋锁⼀直占⽤CPU,在未获得锁的情况下,⼀直⾃旋,相当于死循环,会⼀直 占⽤着CPU,如果不能在很短的时间内获得锁,这⽆疑会使CPU效率降低。 ⽽且⾃旋锁不能实现递归 调⽤ 。
⾃旋锁优先级反转的bug 当多个线程有优先级的时候,如果⼀个优先级低的线程先去访问某个数据,此时使⽤⾃旋锁进⾏了 加锁,然后⼀个优先级⾼的线程⼜去访问这个数据,那么优先级⾼的线程因为优先级⾼会⼀直占着 CPU资源,此时优先级低的线程⽆法与优先级⾼的线程争夺 CPU 时间,从⽽导致任务迟迟完不成、锁⽆法释放。
由于⾃旋锁本身存在的这个问题,所以苹果在iOS10以后已经废弃了OSSpinLock。
也就是说除⾮⼤家能保证访问锁的线程全部都处于同⼀优先级,否则 iOS 系统中的⾃旋锁就不要去使⽤了。
NSCondition存在的虚假唤醒当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件不满⾜时,就会发⽣虚假唤醒。
之所以称为虚假,是因为该线程似乎⽆缘⽆故地被唤醒了。但是虚假唤醒不会⽆缘⽆故发⽣:它们 通常是因为在发出条件变量信号和等待线程最终运⾏之间,另⼀个线程运⾏并更改了条件。线程之 间存在竞争条件,典型的结果是有时,在条件变量上唤醒的线程⾸先运⾏,赢得竞争,有时它运⾏
第⼆,失去竞争。
在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变 量发出信号时等待它,系统可能会决定将它们全部唤醒,将每个signal( )唤醒⼀个线程视为 broadcast( )唤醒所有这些,从⽽打破了信号和唤醒之间任何可能预期的 1:1 关系。如果有 10 个线 程在等待,那么只有⼀个会获胜,另外 9 个会经历虚假唤醒。
读写锁
读写锁的⽬的: 多读单写:在同⼀时刻可以被多条线程进⾏读取数据的操作,但是在同⼀时刻只能有⼀条线程在写⼊数据。
读写互斥:在同以时刻,读和写不能同时进⾏。
文章链接
大家都在找:
ios:ios18吧
objective-c:objective
Xcode:xcode windows版下载
发表评论