在多线程一:GCD中我们详细了解了GCD
,如果多个线程同时占用一块资源,很可能会发生数据错乱和数据安全问题.所以我们今天了解一下线程同步概念.
- 1:
OSSpinLock
导入#import
OSSpinLock
叫做自旋锁
,等待锁的线程会处于忙等 (busy-wait)
状态,一直占用着CPU资源,相当于while(1)
循环;用法如下:
@interface OSSPinTest ()
@property (nonatomic,assign)OSSpinLock moneyLock;
@end
@implementation OSSPinTest
- (instancetype)init{
if (self = [super init]) {
self.moneyLock = OS_SPINLOCK_INIT;
}
return self;
}
//存钱
- (void)saveMoney
//尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
// OSSpinLockTry(&_moneyLock);
//加锁
OSSpinLockLock(&_moneyLock);
[super saveMoney];
解锁
OSSpinLockUnlock(&_moneyLock);
}
//取钱
- (void)drawMoney{
//尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
// OSSpinLockTry(&_moneyLock);
//加锁
[super drawMoney];
//解锁
OSSpinLockUnlock(&_moneyLock);
}
需要注意的是,这种锁在iOS10.0后已经被弃用了,因为这种锁可能会出现优先级反转的问题,如果优先级低的线程抢到了这把锁,给这把锁加锁后,优先级高的线程就会一直处于等待状态,会一直占用CPU资源,优先级低的线程就无法释放
.
- 2:
os_unfair_lock
导入#import
os_unfair_lock
用于取代不安全的OSSpinLock
,从iOS10.0才开始支持.但是和等待OSSpinLock
锁的线程会处于忙等状态不同的是,等待os_unfair_lock
锁的线程处于休眠状态.并非忙等.
@interface OSUnfairLock ()
@property (nonatomic,assign)os_unfair_lock moneyLock;
@property (nonatomic,assign)os_unfair_lock ticketLock;
@end
@implementation OSUnfairLock
- (instancetype)init{
if (self = [super init]) {
self.moneyLock = OS_UNFAIR_LOCK_INIT;
self.ticketLock = OS_UNFAIR_LOCK_INIT;
}
return self;
}
- (void)saveMoney{
os_unfair_lock_lock(&_moneyLock);
[super saveMoney];
os_unfair_lock_unlock(&_moneyLock);
}
- (void)drawMoney{
//尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
// os_unfair_lock_trylock(&_moneyLock);
os_unfair_lock_lock(&_moneyLock);
[super drawMoney];
os_unfair_lock_unlock(&_moneyLock);
}
- (void)ticket{
//尝试加锁,如果需要等待就不加锁,直接返回false;如果不需要等待就直接加锁,返回true.
// os_unfair_lock_trylock(&_ticketLock);
os_unfair_lock_lock(&_ticketLock);
[super ticket];
os_unfair_lock_unlock(&_ticketLock);
}
- 3:
Mutex
:互斥锁,等待锁的线程会处于休眠状态.mutex 是跨平台的
@interface MutexLock ()
@property (nonatomic,assign)pthread_mutex_t moneyLock;
@property (nonatomic,assign)pthread_mutex_t ticketLock;
@end
@implementation MutexLock
- (instancetype)init{
if (self = [super init]) {
//创建锁
[self __initMutex:&_moneyLock];
[self __initMutex:&_ticketLock];
}
return self;
}
//创建锁的属性 attr
- (void)__initMutex:(pthread_mutex_t *)mux{
//创建属性
pthread_mutexattr_t muteattr;
pthread_mutexattr_init(&muteattr);
//设置属性
pthread_mutexattr_settype(&muteattr, PTHREAD_MUTEX_DEFAULT);
//属性值有以下几种
// #define PTHREAD_MUTEX_NORMAL 0 //default
// #define PTHREAD_MUTEX_ERRORCHECK 1 //检查错误
// #define PTHREAD_MUTEX_RECURSIVE 2 //递归锁
// #define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
//创建锁
pthread_mutex_init(mux, &muteattr);
//销毁属性
pthread_mutexattr_destroy(&muteattr);
}
- (void)saveMoney{
//尝试加锁
// pthread_mutex_trylock(&_moneyLock);
pthread_mutex_lock(&_moneyLock);
[super saveMoney];
pthread_mutex_unlock(&_moneyLock);
}
- (void)drawMoney{
pthread_mutex_lock(&_moneyLock);
[super drawMoney];
pthread_mutex_unlock(&_moneyLock);
}
- (void)ticket{
pthread_mutex_lock(&_moneyLock);
[super ticket];
pthread_mutex_unlock(&_moneyLock);
}
如果是递归调用需要加锁,可以把属性设置为pthread_mutexattr_settype(&muteattr, PTHREAD_MUTEX_RECURSIVE);
递归锁允许同一个线程对一把锁进行重复加锁.
递归锁的案例
互斥锁有一个更高级的功能:比如说现在有这么一个需求,一个取钱操作和一个存钱操作,取钱操作必须要等到卡里的余额大于0的时候才可以进行.这种时候就需要用到Mutex
中的pthread_cond_t
了,先看代码:
@interface MutexConditionLock ()
@property (nonatomic,assign)int money;//余额
@property (nonatomic,assign)pthread_mutex_t mutex;//互斥锁
@property (nonatomic,assign)pthread_mutexattr_t attr;//互斥锁属性
@property (nonatomic,assign)pthread_cond_t conditaion;//条件
@end
@implementation MutexConditionLock
- (instancetype)init{
if (self == [super init]) {
//创建锁所需的属性
pthread_mutexattr_init(&_attr);
pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_DEFAULT);
//创建互斥锁
pthread_mutex_init(&_mutex, &_attr);
//创建条件所需的属性
pthread_condattr_t condattr;
pthread_condattr_init(&condattr);
//创建等待条件,第二个参数可以直接传 NULL
pthread_cond_init(&_conditaion, &condattr);
}
return self;
}
- (void)otherTest{
[[[NSThread alloc]initWithTarget:self selector:@selector(__drawMoney) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__saveMoney) object:nil]start];
}
//取钱
- (void)__drawMoney{
pthread_mutex_lock(&_mutex);
//如果 余额 为 0,就把锁解开,并进入休眠状态等待,等待其他线程唤醒,一旦唤醒后再次加锁
if (self.money == 0) {
pthread_cond_wait(&_conditaion, &_mutex);
}
self.money -= 50;
NSLog(@"取钱后余额 %d",self.money);
pthread_mutex_unlock(&_mutex);
}
//存钱
- (void)__saveMoney{
pthread_mutex_lock(&_mutex);
sleep(2);
self.money += 100;
NSLog(@"存钱余额 %d",self.money);
//通知唤醒正在休眠等待中的线程
pthread_cond_signal(&_conditaion);
//如果多个线程都在等待信号唤醒就需要用到广播了
// pthread_cond_broadcast(&_conditaion);
pthread_mutex_unlock(&_mutex);
}
- (void)dealloc{
pthread_mutexattr_destroy(&_attr);
pthread_cond_destroy(&_conditaion);
}
运行结果:
2019-12-12 18:08:54.470700+0800 各种lockTest[3159:1068307] 存钱余额 100
2019-12-12 18:08:54.471072+0800 各种lockTest[3159:1068306] 取钱后余额 50
关键代码就在pthread_cond_wait(pthread_cond_t , pthread_mutex_t )
和pthread_cond_signal(pthread_cond_t *)
这两句:
-
pthread_cond_wait(pthread_cond_t , pthread_mutex_t )
传入两个参数,第一个参数就是等待唤醒的条件
;第二个参数是互斥锁
.
以实例代码为例,如果首选进入__drawMoney
方法,然后对_mutex
加锁,如果self.money == 0
符合条件,执行pthread_cond_wait(&_conditaion, &_mutex);
, -
pthread_cond_signal(pthread_cond_t *)
传入一个条件,发送信号唤醒正在等待这个条件的线程.
刚才我们说OSSpinLock
线程会处于忙等状态
,我们从汇编代码看看是不是这样:
si
(step instruction):是让汇编代码一行一行执行.
s
(step):是让OC 代码一行一行执行.
next I
:也是一样一行往下走,只不过遇到函数调用的时候不会进入函数内部,而si
会进入函数内部.所以要想查看函数内部实现就要用si
.
从
OSSpinLock
汇编语言看到,底部就是一个while
循环,一直处于忙等状态.
再来看看Mutex
互斥锁的底层汇编:
os_unfair_lock
的底层汇编:
从汇编可以看到
os_unfair_lock
和Mutex
一样都是让等待的线程进入休眠状态.
另外苹果官方也说
os_unfair_lock
是一个Low-level lock
( 低级锁 ).低级锁的特点就是睡觉.
- 4:
NSLock
:是对Mutex
普通锁封装.只是用起来更加面向对象,更加方便,主要有4个方法:
NSLocking
协议下的lock()
和unlock()
方法以及自身的tryLock()
和lockBeforeDate:
方法.我们只说一下lockBeforeDate:
方法因为其他3个方法和mutex
功能一样.
lockBeforeDate :(NSDate*)date
:传入一个时间,表示在这个时间之前线程会一直等待,如果等到别的线程放开这把锁就对这把锁加锁,并返回yes
;如果在规定的时间还是没有等到这把锁,就加锁失败,返回NO
代码继续往下走.会阻塞线程. - 5:
NSRecursiveLock
是对Mutex
递归锁的封装.API和NSLock
一致. - 6:
NSCodition
是对mutex
和cond
的封装.主要有以下API:
- (void)wait;
等待条件唤醒
- (BOOL)waitUntilDate:(NSDate *)limit;
传入一个时间,在这个时间之前线程一直休眠等待.时间到了之后自动唤醒.
- (void)signal;
信号
- (void)broadcast;
广播
@interface NSConditionTest ()
@property (nonatomic,strong)NSCondition *moneyLock;
@property (nonatomic,assign)int money;//余额
@end
@implementation NSConditionTest
- (instancetype)init{
if (self = [super init]) {
//初始化锁
self.moneyLock = [[NSCondition alloc]init];
}
return self;
}
- (void)otherTest{
[[[NSThread alloc]initWithTarget:self selector:@selector(__drawMoney) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__saveMoney) object:nil]start];
}
//取钱
- (void)__drawMoney{
[self.moneyLock lock];
//如果 余额 为 0,就把锁解开,并进入休眠状态等待,等待其他线程唤醒,一旦唤醒后再次加锁
if (self.money == 0) {
[self.moneyLock wait];
}
self.money -= 50;
NSLog(@"取钱后余额 %d",self.money);
[self.moneyLock unlock];
}
//存钱
- (void)__saveMoney{
[self.moneyLock lock];
sleep(2);
self.money += 100;
NSLog(@"存钱余额 %d",self.money);
//通知唤醒正在休眠等待中的线程
[self.moneyLock signal];
//如果多个线程都在等待信号唤醒就需要用到广播了
// pthread_cond_broadcast(&_conditaion);
[self.moneyLock unlock];
}
@end
运行结果
2019-12-13 10:25:50.905652+0800 各种lockTest[3621:1439700] 存钱余额 100
2019-12-13 10:25:50.905983+0800 各种lockTest[3621:1439699] 取钱后余额 50
- 7:
NSConditionLock
:是对NSCondition
的进一步封装,可以设置具体的条件值.可以控制线程间的执行顺序.主要API如下:
@property (readonly) NSInteger condition;
条件值
- (void)lockWhenCondition:(NSInteger)condition;
一直等到符合条件后加锁
- (BOOL)tryLock;
尝试加锁,如果锁已被其他线程加锁立马返回NO;如果未被加锁就加锁后返回YES.
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
尝试解锁,如果锁被其他线程占用立马返回NO,否则返回YES.
- (void)unlockWithCondition:(NSInteger)condition;
释放锁,并设置条件值
- (BOOL)lockBeforeDate:(NSDate *)limit;
是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
一直等待符合条件值后加锁.
@interface NSConditionLockDemo ()
@property (nonatomic,strong)NSConditionLock *lock;
@end
@implementation NSConditionLockDemo
- (instancetype)init{
if (self = [super init]) {
//初始化锁,条件值设为1
self.lock = [[NSConditionLock alloc]initWithCondition:1];
}
return self;
}
- (void)otherTest{
[[[NSThread alloc]initWithTarget:self selector:@selector(__test1) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(__test2) object:nil]start];
}
- (void)__test1{
//如果条件值为1,就加锁
[self.lock lockWhenCondition:1];
NSLog(@"1");
sleep(2);
//解锁,并把条件值设为2
[self.lock unlockWithCondition:2];
}
- (void)__test2{
//如果条件值为2,就加锁
[self.lock lockWhenCondition:2];
NSLog(@"2");
[self.lock unlock];
}
@end
运行结果
2019-12-13 21:00:55.954512+0800 各种lockTest[4907:2111963] 1
2019-12-13 21:00:57.960116+0800 各种lockTest[4907:2111964] 2
注意如果initWithCondition
创建的时候条件值没有设置或设置的nil
,condition
默认是0;
- 8:
dispatch_semaphore
:可以用来控制线程并发访问的最大数量.
@interface SemaphoreDemo ()
@property (nonatomic,strong)dispatch_semaphore_t semaphore;
@property (nonatomic,strong)dispatch_semaphore_t money_semaphore;
@property (nonatomic,strong)dispatch_semaphore_t ticket_semaphore;
@end
@implementation SemaphoreDemo
- (instancetype)init{
if (self = [super init]) {
//初始化锁,条件值设为1
self.semaphore = dispatch_semaphore_create(5);
self.money_semaphore = dispatch_semaphore_create(1);
self.ticket_semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)saveMoney{
dispatch_semaphore_wait(self.money_semaphore, DISPATCH_TIME_FOREVER);
[super saveMoney];
dispatch_semaphore_signal(self.money_semaphore);
}
- (void)drawMoney{
dispatch_semaphore_wait(self.money_semaphore, DISPATCH_TIME_FOREVER);
[super drawMoney];
dispatch_semaphore_signal(self.money_semaphore);
}
- (void)ticket{
dispatch_semaphore_wait(self.ticket_semaphore, DISPATCH_TIME_FOREVER);
[super ticket];
dispatch_semaphore_signal(self.ticket_semaphore);
}
- (void)otherTest{
for (int i = 0; i < 30; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(__test1) object:nil]start];
}
}
- (void)__test1{
//信号量的值 -1 继续往下执行代码
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_NOW);
sleep(2);
NSLog(@"1111");
//信号量的值 +1
dispatch_semaphore_signal(_semaphore);
}
@end
如果设置semaphore
的初始值为5,就代表线程并发访问的最大值是5.他的实现原理是:如果信号量的初始值 <= 0,当前线程就会进入休眠状态等待,直到信号量的值 > 0;如果信号量的值 > 0,就先减1,然后往下执行代码.
dispatch_semaphore_signal 会让信号量的值加1
.
所以如果,设置信号量的值为1,控制线程的最大并发数为1,就可以实现线程同步
- 9:
dispatch_queue
使用GCD的串行队列实现线程同步.把需要控制的操作都放到一个串行队列中:
@interface SerialQueueDemo ()
@property (nonatomic,strong)dispatch_queue_t serialQueue_money;
@property (nonatomic,strong)dispatch_queue_t serialQueue_ticket;
@end
@implementation SerialQueueDemo
- (instancetype)init{
if (self == [super init]) {
self.serialQueue_money = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
self.serialQueue_ticket = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)saveMoney{
dispatch_sync(self.serialQueue_money, ^{
[super saveMoney];
});
}
- (void)drawMoney{
dispatch_sync(self.serialQueue_money, ^{
[super drawMoney];
});
}
- (void)ticket{
dispatch_sync(self.serialQueue_ticket, ^{
[super ticket];
});
}
@end
- 10:
synchronized
:是对mutex
递归锁的封装.这是最简洁的方式,但是性能比较差,苹果不推荐使用.
@implementation SynchronizedDemo
- (void)saveMoney{
//传入的对象要保证是同一个对象
@synchronized (self) { // objc_sync_enter 相当于加锁
[super saveMoney];
}// objc_sync_exit 相当于解锁
}
- (void)drawMoney{
@synchronized (self) {//加锁
[super drawMoney];
}//解锁
}
- (void)ticket{
static NSObject *lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSObject alloc]init];
});
@synchronized (lock) {
[super ticket];
}
}
@end
跟进synchronized
的汇编代码,会发现两个重要的函数:
在objc-sync.mm
中找到这两个函数:
到目前为止我们已经讲了10中线程同步的方法,那么我们在项目中应该使用哪一种呢:
使用小技巧:
如果有好几个方法都需要加不同的锁,我们可以这样写:
- (void)test{
static dispatch_semaphore_t semaphore;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
semaphore = dispatch_semaphore_create(1);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//..
//加锁操作
//...
dispatch_semaphore_signal(semaphore);
}
也可以把加锁的代码写成宏定义,这样更方便:
- (void)test{
DispatchSemaphoreBegin
//..
//加锁操作
//...
DispatchSemaphoreEnd
}
自旋锁和互斥锁的比较:
什么情况下使用自旋锁比较划算?
- 预计线程等待的时间很短.
如果线程等待的时间很短,就没必要让线程休眠等待,因为休眠后再唤醒也会消耗资源,降低性能. - 加锁的代码(临界区)经常被调用,但竞争情况很少发生.
临界区:lock
和unlock
之间的代码我们称之为临界区. - CPU资源不紧张
什么情况下使用互斥锁比较划算? - 预计线程等待锁的时间比较长,比如说2,3s.
- 单核处理器.
- 临界区有IO操作.
IO操作的时间一般比较长,需要更多的CPU资源,而自旋锁会一直占用CPU资源,我们应该把CPU资源让出来给IO操作,所以IO操作用互斥锁比较合适. - 临界区代码比较复杂或者循环量大.
- 临界区的竞争非常激烈.
iOS中实现多读单写
比如说现在有这种需求:
1:同一时间只能有一个线程进行写入文件的操作.
2:同一时间允许多个线程进行读取文件的操作.
3:同一时间,不允许读,写操作同时进行.
上面的需求就是多读单写
操作,iOS中有两种方式实现多读单写操作:
-
pthread_rwlock
:读写锁
@interface PthreadRWlLockDemo ()
@property (nonatomic,assign)pthread_rwlock_t rwLock;
@end
@implementation PthreadRWlLockDemo
- (instancetype)init{
if (self == [super init]) {
//初始化读写锁
pthread_rwlock_init(&_rwLock, NULL);
}
return self;
}
- (void)otherTest{
for (int i = 0; i < 10; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
}
}
- (void)read{
//读操作加锁
pthread_rwlock_rdlock(&_rwLock);
sleep(1);
NSLog(@"read");
//解锁
pthread_rwlock_unlock(&_rwLock);
}
- (void)write{
//写操作加锁
pthread_rwlock_wrlock(&_rwLock);
sleep(1);
NSLog(@"write");
解锁
pthread_rwlock_unlock(&_rwLock);
}
@end
-
dispatch_barrier_async
:异步栅栏调用
@interface BarrierLockDemo ()
@property (nonatomic,strong)dispatch_queue_t queue;
@end
@implementation BarrierLockDemo
- (instancetype)init{
if (self == [super init]) {
//初始化读写锁
self.queue = dispatch_queue_create("readWirteQueue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)otherTest{
for (int i = 0; i < 10; i ++) {
[[[NSThread alloc]initWithTarget:self selector:@selector(read) object:nil]start];
[[[NSThread alloc]initWithTarget:self selector:@selector(write) object:nil]start];
}
}
- (void)read{
dispatch_async(self.queue, ^{
sleep(1);
NSLog(@"read");
});
}
- (void)write{
dispatch_barrier_async(self.queue, ^{
sleep(1);
NSLog(@"write");
});
}
@end
异步栅栏的原理是:把写入文件
的任务放到队列的时候,会给这个线程建立一个栅栏,围栏
,不允许其他的任务进来.如图:
使用异步栅栏的时候需要注意:传入这个函数的队列( queue )必须是通过
dispatch_queue_create
创建的,如果传入的是一个串行或者全局并发队列,那异步栅栏函数的功能就相当于dispatch_asyn
的效果.
-
atomic
:最后说一下atomic
关键字.
atomic
是线程安全的,如果使用这个关键字修饰属性,系统会在属性的setter
,getter
方法内部加上加锁
和解锁
的代码,我们看一下源代码:
get 方法底层实现
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;// 如果是 nonatomic 直接返回值
// Atomic retain release world
//如果是 atomic
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);
}
set 方法底层实现
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) {//如果是 nonatomic
oldValue = *slot; //*slot 属性的内存地址
*slot = newValue;
} else {// atomic
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();//加锁
oldValue = *slot;
*slot = newValue;
slotlock.unlock();//解锁
}
objc_release(oldValue);
}
虽然atomic
是线程安全的,但是我们在项目中还是不会使用,因为我们会非常频繁的访问属性,如果属性用atomic
修饰,那会极大的消耗性能.所以我们项目中一般都是用nonatomic
,如果有的属性的确需要线程同步操作,完全可以哪里需要哪里加锁.