内容简介:锁:是保证线程安全常见的同步工具,防止Data race(数据竞争)的发生。其中,锁的属性包含一下四种:实例代码:
在IOS上进行多线程开发,为了保证线程安全,防止资源竞争,需要给进程进行加锁,通常用到的进程锁分为7种。
- 信号量
- 互斥锁
- 自旋锁
- 递归锁
- 条件锁
- 读写锁
- 分布式锁
锁
锁:是保证线程安全常见的同步工具,防止Data race(数据竞争)的发生。
Data race(数据竞争):
- 两个或者更多线程在一个程序中,并发的访问同一数据
- 至少一个访问是写入操作
- 些线程都不使用任何互斥锁来控制这些访问
pthread_mutex
pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // 定义锁的属性 pthread_mutex_t mutex; pthread_mutex_init(&mutex, &attr) // 创建锁 pthread_mutex_lock(&mutex); // 申请锁 pthread_mutex_unlock(&mutex); // 释放锁 复制代码
其中,锁的属性包含一下四种:
PTHREAD_MUTEX_NORMAL:默认值普通锁,当一个线程加锁以后,其他线程进入按照优先顺序进入等待队列,并且解锁的时候按照先入先出的方式获得锁。 PTHREAD_MUTEX_ERRORCHECK:检错锁,当同一个线程获得同一个锁的时候,则返回EDEADLK,否则与普通锁处理一样。 PTHREAD_MUTEX_RECURSIVE:递归锁。这里有别于上面的检错锁,同一个线程可以递归获得锁,但是加锁和解锁必须要一一对应。 PTHREAD_MUTEX_DEFAULT:适应锁,等待解锁之后重新竞争,没有等待队列。 复制代码
信号量
dispatch_semaphore
是GCD用来同步的一种方式, dispatch_semephore_create
方法用户创建一个 dispatch_semephore_t
类型的信号量,初始的参数必须大于0,该参数用来表示该信号量有多少个信号,简单的说也就是同事允许多少个线程访问。 dispatch_semaphore_wait
方法是等待一个信号量,该方法会判断 signal
的信号值是否大于0,如果大于0则不会阻塞线程,消耗点一个信号值,执行后续任务。如果信号值等于0那么就和 NSCondition
一样,阻塞当前线程进入等待状态,如果等待时间未超过timeout并且 dispatch_semaphore_signal
释放了了一个信号值,那么就会消耗掉一个信号值并且向下执行。如果期间一直不能获得信号量并且超过超时时间,那么就会自动执行后续语句。
dispatch_semaphore_create(long value) dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) dispatch_semaphore_signal(dispatch_semaphore_t dsema)
实例代码:
- (void)semaphoreSync { NSLog(@"semaphore---begin"); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //创建初始信号量 为 0 ,阻塞所有线程 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); __block int number = 0; dispatch_async(queue, ^{ // 追加任务A [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程 number = 100; // 执行完线程,信号量加 1,信号总量从 0 变为 1 dispatch_semaphore_signal(semaphore); }); //原任务B ////若计数为0则一直等待,直到接到总信号量变为 >0 ,继续执行后续代码 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"semaphore---end,number = %d",number); } 复制代码
互斥锁
互斥锁的实现原理与信号量非常相似,不是使用忙等,而是阻塞线程并睡眠,需要进行上下文切换。
当一个线程获得这个锁之后,其他想要获得此锁的线程将会被阻塞,直到该锁被释放。
当临界区加上互斥锁以后,其他的调用方不能获得锁,只有当互斥锁的持有方释放锁之后其他调用方才能获得锁。
调用方在获得锁的时候发现互斥锁已经被其他方持有,那么该调用方只能进入睡眠状态,这样不会占用CPU资源。但是会有时间的消耗,系统的运行时基于CPU时间调度的,每次线程可能有100ms的运行时间,频繁的CPU切换也会消耗一定的时间。
NSLock:
NSLock遵循NSLocking协议,同时也是互斥锁,提供了 lock
和 unlock
方法来进行加锁和解锁。
NSLock内部是封装了 pthread_mutext
,类型是 PTHREAD_MUTEXT_ERRORCHECK
,它会损失一定的性能换来错误提示。
- (void)lock; - (void)unlock; - (BOOL)tryLock; - (BOOL)lockBeforeDate:(NSDate *)limit; 复制代码
tryLock
和 lock
方法都会请求加锁,唯一不同的是 trylock
在没有获得锁的时候可以继续做一些任务和处理。 lockBeforeDate:
方法也比较简单,就是在limit时间点之前获得锁,没有拿到锁就返回NO。
@synchronized:
这其实是一个 OC 层面的锁,防止不同的线程同时执行同一段代码,相比于使用 NSLock 创建锁对象、加锁和解锁来说, @synchronized
用着更方便,可读性更高。
大体上,想要明白 @synchronized
,需要知道在 @synchronized
中 objc_sync_enter
和 objc_sync_exit
的成对调用,而且每个传入的对象,都会为其分配一个递归锁并存储在哈希表中。在 objc_sync_enter
中加锁,在 objc_sync_exit
中解锁。
具体可以参考这篇文章: 关于 @synchronized,这儿比你想知道的还要多
@synchronized(self) { //数据操作 } 复制代码
自旋锁
自旋锁的目的是为了确保临界区只有一个线程可以访问。
当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放,是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
由于调用方会一直循环看该自旋锁的的保持者是否已经释放了资源,所以总的效率来说比互斥锁高。但是自旋锁只用于短时间的资源访问,如果不能短时间内获得锁,就会一直占用着CPU,造成效率低下。
OSSpinLock:
自旋锁的一种,由于在某些场景下不安全已被弃用。
需导入头文件 #import <libkern/OSAtomic.h>
OSSpinLock lock = OS_SPINLOCK_INIT; OSSpinLockLock(&lock); OSSpinLockUnlock(&lock); OSSpinLockTry(&lock); 复制代码
自旋锁存在优先级反转问题, OSSpinLock
是自旋锁,也正是由于它是自旋锁,所以容易发生优先级反转的问题。在ibireme的文章中已经写到,当一个低优先级线程获得锁的时候,如果此时一个高优先级的系统到来,那么会进入忙等状态,不会进入睡眠,此时会一直占用着系统CPU时间,导致低优先级的无法拿到CPU时间片,从而无法完成任务也无法释放锁。除非能保证访问锁的线程全部处于同一优先级,否则系统所有的自旋锁都会出现优先级反转的问题。现在苹果的 OSSpinLock
已经被替换成
os_unfair_lock typedef int32_t OSSpinLock OSSPINLOCK_DEPRECATED_REPLACE_WITH(os_unfair_lock); 复制代码
os_unfair_lock:
os_unfair_lock
是苹果官方推荐的替换 OSSpinLock
的方案,用于解决 OSSpinLock
优先级反转问题,但是它在iOS10.0以上的系统才可以调用。
导入头文件 #import< os/lock.h >
os_unfair_lock_t unfairLock; unfairLock = &(OS_UNFAIR_LOCK_INIT); os_unfair_lock_lock(unfairLock); os_unfair_lock_unlock(unfairLock); os_unfair_lock_trylock(unfairLock); 复制代码
递归锁
需要使用递归锁的情况:
NSLock *lock = [[NSLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ static void (^RecursiveLock)(int); RecursiveLock = ^(int value) { [lock lock]; if (value > 0) { NSLog(@"value = %d", value); sleep(2); RecursiveLock(value - 1); } [lock unlock]; }; RecursiveLock(5); }); 复制代码
这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所有每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所有它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。导致crach
*** -[NSLock lock]: deadlock ( '(null)') *** Break on _NSLockError() to debug. 复制代码
NSRecursiveLock
递归锁也是通过 pthread_mutex_lock
函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,锁的释放过程也是同理。
一个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。 NSRecursiveLock
与 NSLock
的区别在于内部封装的 pthread_mutex_t
对象的类型不同,前者的类型为 PTHREAD_MUTEX_RECURSIVE
。
主要操作:
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; [lock lockBeforeDate:date]; [lock tryLock];
另外,NSRecursiveLock还声明了一个name属性,如下:
@property(copy) NSString *name
我们可以使用这个字符串来标识一个锁。Cocoa也会使用这个name作为错误描述信息的一部分。
条件锁
NSCondition
封装了一个互斥锁和信号量,它把前者的 lock
以及后者的 wait
/ signal
统一到 NSCondition
对象中,是基于条件变量 pthread_cond_t
来实现的,和信号量相似,如果当前线程不满足条件,那么就会进入睡眠状态,等待其他线程释放锁或者释放信号之后,就会唤醒线程。
NSCondition 的对象实际上作为一个锁和一个线程检查器:锁主要为了当检测条件时保护数据源,执行条件引发的任务;线程检查器主要是根据条件决定是否继续运行线程,即线程是否被阻塞。
-
NSCondition
同样实现了NSLocking
协议,所以它和NSLock
一样,也有NSLocking协议的lock
和unlock
方法,可以当做NSLock
来使用解决线程同步问题,用法完全一样。 -
NSCondition
提供了wait
和signal
,和条件信号量类似。比如我们要监听array数组的个数,当array的个数大于0的时候就执行清空操作。思路是这样的,当array个数大于0时执行清空操作,否则,wait
等待执行清空操作。当array个数增加的时候发生signal
信号,让等待的线程唤醒继续执行。NSCondition *lock = [[NSCondition alloc] init]; NSMutableArray *array = [[NSMutableArray alloc] init]; //消费者 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [lock lock]; while (!array.count) { [lock wait]; } [array removeAllObjects]; [lock unlock]; }); //生产者 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1);//以保证让线程2的代码后执行 [lock lock]; [array addObject:@1]; [lock signal]; [lock unlock]; }); 复制代码
-
NSCondition
可以给每个线程分别加锁,加锁后不影响其他线程进入临界区。但是正是因为这种分别加锁的方式,NSCondition使用wait并使用加锁后并不能真正的解决资源的竞争。例如:不能让m<0。假设当前m=0,线程A要判断到m>0为假,执行等待;线程B执行了m=1操作,并唤醒线程A执行m-1操作的同时线程C判断到m>0,因为他们在不同的线程锁里面,同样判断为真也执行了m-1,这个时候线程A和线程C都会执行m-1,但是m=1,结果就会造成m=-1.
NSCoditionLock
NSConditionLock也可以像NSCondition一样做多线程之间的任务等待调用,而且是线程安全的。
NSConditionLock同样实现了NSLocking协议,性能比较低。
NSConditonLock
内部持有了一个 NSCondition
对象和 _condition_value
属性,当调用
- (instancetype)initWithCondition:(NSInteger)condition
初始化的时候会传入一个 condition
参数,该参数会赋值 _condition_value
属性
常用方法:
-
lock
不分条件,如果锁没被申请,直接执行代码 -
lockBeforeDate:
在指定时间前尝试加锁,返回bool
-
lockWhenCondition:
满足特定条件Condition
,加锁执行相应代码 -
lockWhenCondition: beforeDate:
和上条相同,增加时间戳 -
tryLock
尝试着加锁,返回bool
-
tryLockWhenCondition:
,满足特定条件Condition
,尝试着加锁,返回bool
-
unlock
不会清空条件,之后满足条件的锁还会执行 -
unlockWithCondition:
设置解锁条件(同一时刻只有一个条件,如果已经设置条件,相当于修改条件)
实例:
- (void)executeNSConditionLock { NSConditionLock* lock = [[NSConditionLock alloc] init]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (NSUInteger i=0; i<3; i++) { sleep(2); if (i == 2) { [lock lock]; [lock unlockWithCondition:i]; } } }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1); [self threadMethodOfNSCoditionLock:lock]; }); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ sleep(1); [self threadMethodOfNSCoditionLock:lock]; }); } -(void)threadMethodOfNSCoditionLock:(NSConditionLock*)lock{ [lock lockWhenCondition:2]; [lock unlock]; } 复制代码
读写锁
读写锁,在对文件进行操作的时候,写操作是排他的,一旦有多个线程对同一个文件进行写操作,后果不可估量,但读是可以的,多个线程读取时没有问题的。
pthread_rwlock
读写锁可以有三种状态:
- 读模式下加锁状态,
- 写模式下加锁状态,
- 不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可用同时占有读模式的读写锁。读写锁也叫做 共享-独占锁 ,当读写锁以读模式锁住时,它是以共享模式锁住的,当它以写模式锁住时,它是以独占模式锁住的。
因此:
- 当读写锁被一个线程以读模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程还可以继续进行
- 当读写锁被一个线程以写模式占用的时候,写操作的其他线程会被阻塞,读操作的其他线程也被阻塞。
注意:
- 如果自己已经获取了读锁,再去加写锁,会出现死锁的
// 初始化 pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; //获取一个读出锁 int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr); //获取一个写入锁 int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr); int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr); int pthread_rwlock_unlock(pthread_rwlock_t *rwptr); //释放一个写入锁或者读出锁 //读写锁属性: int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr) int pthread_rwlock_destroy(pthread_rwlock_t *rwptr); int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr); int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int valptr); 复制代码
实例:
// 初始化 pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER // 读模式 pthread_rwlock_wrlock(&lock); // 写模式 pthread_rwlock_rdlock(&lock); // 读模式或者写模式的解锁 pthread_rwlock_unlock(&lock); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self readBookWithTag:1]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self readBookWithTag:2]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self writeBook:3]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self writeBook:4]; }); dispatch_async(dispatch_get_global_queue(0, 0), ^{ [self readBookWithTag:5]; }); - (void)readBookWithTag:(NSInteger )tag { pthread_rwlock_rdlock(&rwLock); self.path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@".doc"]; self.contentString = [NSString stringWithContentsOfFile:self.path encoding:NSUTF8StringEncoding error:nil]; pthread_rwlock_unlock(&rwLock); } - (void)writeBook:(NSInteger)tag { pthread_rwlock_wrlock(&rwLock); [self.contentString writeToFile:self.path atomically:YES encoding:NSUTF8StringEncoding error:nil]; pthread_rwlock_unlock(&rwLock); } 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Java™ 教程(进程和线程)
- 进程、线程、协程和goroutine
- Python第十二章-多进程和多线程01-多进程
- Linux 线程(进程)数限制分析
- 进程与线程:入门知识篇
- Linux 内核初探:进程与线程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。