数据安全隐患
- 资源共享
- 一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
- 比如多个线程访问同一个对象、同一个变量、同一个文件
- 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题
如以下案例,会造成数据安全问题:
#import <Foundation/Foundation.h>
@interface MJBaseDemo : NSObject
- (void)moneyTest;
- (void)ticketTest;
#pragma mark - 暴露给子类去使用
- (void)__saveMoney;
- (void)__drawMoney;
- (void)__saleTicket;
@end
#import "MJBaseDemo.h"
@interface MJBaseDemo()
@property (assign, nonatomic) int money;
@property (assign, nonatomic) int ticketsCount;
@end
@implementation MJBaseDemo
/**
存钱、取钱演示
*/
- (void)moneyTest
{
self.money = 100;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self __saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 10; i++) {
[self __drawMoney];
}
});
}
/**
存钱
*/
- (void)__saveMoney
{
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
/**
取钱
*/
- (void)__drawMoney
{
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取20,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
/**
卖1张票
*/
- (void)__saleTicket
{
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}
/**
卖票演示
*/
- (void)ticketTest
{
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self __saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self __saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self __saleTicket];
}
});
}
@end
调用
- (void)viewDidLoad {
[super viewDidLoad];
MJBaseDemo *demo = [[OSSpinLockDemo2 alloc] init];
[demo ticketTest];
[demo moneyTest];
}
以上卖票问题和存取钱案例,会造成数据安全问题
解决方案: 线程同步技术
常见线程同步方案
OSSpinLock
-
OSSpinLock叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源(相当于 while(已上锁);)
-
目前已经不再安全,可能会出现优先级反转问题。
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
- 需要导入头文件
#import<libkern/OSAtomic.h>
代码如下,OSSpinkLockDemo继承MJBaseDemo
#import "OSSpinLockDemo.h"
#import <libkern/OSAtomic.h>
@interface OSSpinLockDemo()
@property (assign, nonatomic) OSSpinLock moneyLock;
@property (assign, nonatomic) OSSpinLock ticketLock;
@end
@implementation OSSpinLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.moneyLock = OS_SPINLOCK_INIT;
self.ticketLock = OS_SPINLOCK_INIT;
}
return self;
}
- (void)__drawMoney
{
OSSpinLockLock(&_moneyLock);
[super __drawMoney];
OSSpinLockUnlock(&_moneyLock);
}
- (void)__saveMoney
{
OSSpinLockLock(&_moneyLock);
[super __saveMoney];
OSSpinLockUnlock(&_moneyLock);
}
- (void)__saleTicket
{
OSSpinLockLock(&_ticketLock);
[super __saleTicket];
OSSpinLockUnlock(&_ticketLock);
}
@end
-
每个线程访问的锁必须是同一把锁。线程1进来访问这把锁,发现没有加锁,然后加锁-->处理业务-->解锁,这时线程2进来访问这把锁,发现已加锁,就处于忙等状态。这时就达到了目的。这里的锁是属性。如果锁是局部变量,每次进来都初始化一下,线程1进来锁是未锁状态,线程2进来,此时锁已经不是之前的锁了,也是未锁状态,就达不到目的。
-
线程阻塞有两种方案,1.线程休眠 2.忙等,相当于相当于 while(已上锁);一直占用CPU资源。
-
自旋锁目前已不再安全。比如有三个线程,线程1、线程2、线程3,系统会进行线程调度,分出很小的时间片,先执行线程1,再执行线程2...这样就能达到“微观串行,宏观并发”。优先级较高的线程,会分出比较多的时间片去执行。如果此时,线程2先进来,加锁。同时,线程1进来处于忙等状态,因为线程1优先级高,可能CPU一直会分出时间片给线程1,这时线程1处于忙等状态,线程2没有CPU资源也无法解锁。所以,可能会造成死锁。线程和进程都会有这种时间片轮转调度算法。
-
自旋锁已经不安全了,不推荐使用。但是效率还是比较高的,因为忙等,不会让线程休眠。如果线程休眠再唤醒也是消耗性能的。
-
还可以用另一个API,尝试加锁。加锁成功,就处理业务,然后解锁,尝试加锁失败,就处于忙等。
// if (OSSpinLockTry(&_lock)) {
// int oldTicketsCount = self.ticketsCount;
// sleep(.2);
// oldTicketsCount--;
// self.ticketsCount = oldTicketsCount;
// NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
//
// OSSpinLockUnlock(&_lock);
// }
- 这两个锁也可以不用属性,而是用static 全局变量和static局部变量。只要只初始化一次,使每次访问的锁是同一把锁就可以。
#import "OSSpinLockDemo2.h"
#import <libkern/OSAtomic.h>
@implementation OSSpinLockDemo2
static OSSpinLock moneyLock_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
moneyLock_ = 0;
});
}
- (void)__drawMoney
{
OSSpinLockLock(&moneyLock_);
[super __drawMoney];
OSSpinLockUnlock(&moneyLock_);
}
- (void)__saveMoney
{
OSSpinLockLock(&moneyLock_);
[super __saveMoney];
OSSpinLockUnlock(&moneyLock_);
}
- (void)__saleTicket
{
// static NSString *str = nil;
// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
// str = [NSString stringWithFormat:@"123"];
// });
static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&ticketLock);
[super __saleTicket];
OSSpinLockUnlock(&ticketLock);
}
@end
static NSString *str = [NSString stringWithFormat:@"123"]; 会报错。static静态初始化,右值必须是直接值,编译阶段就确定它的值,函数调用是运行阶段才知道它的值。上面可以改造成这样:
// static NSString *str = nil;
// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
// str = [NSString stringWithFormat:@"123"];
// });
os_unfair_lock
- os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持
- 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
- 需要导入头文件
#import <os/lock.h>
demo:
OSUnfairLockDemo继承MJBaseDemo
#import "OSUnfairLockDemo.h"
#import <os/lock.h>
@interface OSUnfairLockDemo()
// Low-level lock
// ll lock
// lll
// Low-level lock的特点等不到锁就休眠
@property (assign, nonatomic) os_unfair_lock moneyLock;
@property (assign, nonatomic) os_unfair_lock ticketLock;
@end
@implementation OSUnfairLockDemo
- (instancetype)init
{
if (self = [super init]) {
self.moneyLock = OS_UNFAIR_LOCK_INIT;
self.ticketLock = OS_UNFAIR_LOCK_INIT;
}
return self;
}
- (void)__saleTicket
{
os_unfair_lock_lock(&_ticketLock);
[super __saleTicket];
os_unfair_lock_unlock(&_ticketLock);
}
- (void)__saveMoney
{
os_unfair_lock_lock(&_moneyLock);
[super __saveMoney];
os_unfair_lock_unlock(&_moneyLock);
}
- (void)__drawMoney
{
os_unfair_lock_lock(&_moneyLock);
[super __drawMoney];
os_unfair_lock_unlock(&_moneyLock);
}
@end
pthread_mutex
pthread开头的都是跨平台的
- mutex叫做“互斥锁”,等待锁的线程会处于休眠状态
- 需要导入头文件 #import <pthread.h>
DEMO:
#import "MutexDemo.h"
#import <pthread.h>
@interface MutexDemo()
@property (assign, nonatomic) pthread_mutex_t ticketMutex;
@property (assign, nonatomic) pthread_mutex_t moneyMutex;
@end
@implementation MutexDemo
- (void)__initMutex:(pthread_mutex_t *)mutex
{
// // 初始化属性
// pthread_mutexattr_t attr;
// pthread_mutexattr_init(&attr);
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// // 初始化锁
// pthread_mutex_init(mutex, &attr);
// // 销毁属性
// pthread_mutexattr_destroy(&attr);
// 初始化锁
pthread_mutex_init(mutex, NULL);
}
- (instancetype)init
{
if (self = [super init]) {
// self.moneyMutex = PTHREAD_MUTEX_INITIALIZER; //会报错 结构体可以静态初始化,self.moneyMutex是运行的时候调setter方法
// pthread_mutex_t mutex;
// mutex = PTHREAD_MUTEX_INITIALIZER; //会报错
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //这样是可以的
[self __initMutex:&_ticketMutex];
[self __initMutex:&_moneyMutex];
}
return self;
}
// 死锁:永远拿不到锁
- (void)__saleTicket
{
pthread_mutex_lock(&_ticketMutex);
[super __saleTicket];
pthread_mutex_unlock(&_ticketMutex);
}
- (void)__saveMoney
{
pthread_mutex_lock(&_moneyMutex);
[super __saveMoney];
pthread_mutex_unlock(&_moneyMutex);
}
- (void)__drawMoney
{
pthread_mutex_lock(&_moneyMutex);
[super __drawMoney];
pthread_mutex_unlock(&_moneyMutex);
}
- (void)dealloc
{
pthread_mutex_destroy(&_moneyMutex);
pthread_mutex_destroy(&_ticketMutex);
}
@end
- 结构体变量赋值是不能大括号赋值的,初始化时可以用大括号就行初始化。
// pthread_mutex_t mutex;
// mutex = PTHREAD_MUTEX_INITIALIZER; //会报错
所以
// self.moneyMutex = PTHREAD_MUTEX_INITIALIZER; //会报错 结构体可以静态初始化,self.moneyMutex是运行的时候调setter方法
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //这样是可以的
- pthread_mutex初始化:
// // 初始化属性
// pthread_mutexattr_t attr;
// pthread_mutexattr_init(&attr);
// pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// // 初始化锁
// pthread_mutex_init(mutex, &attr);
// // 销毁属性
// pthread_mutexattr_destroy(&attr);
如上面,锁的类型是默认时,初始化可以简化成:pthread_mutex_init(mutex, NULL);
- 注意在
dealloc里销毁。
pthread_mutex 递归锁
- (void)otherTest
{
pthread_mutex_lock(&_mutex);
NSLog(@"%s", __func__);
static int count = 0;
if (count < 10) {
count++;
[self otherTest];
}
pthread_mutex_unlock(&_mutex);
}
调otherTest时会出现死锁
- (void)viewDidLoad {
[super viewDidLoad];
MJBaseDemo *demo = [[NSConditionDemo alloc] init];
// [demo ticketTest];
// [demo moneyTest];
[demo otherTest];
}
这时就需要把pthread_mutex的锁的类型换成递归锁
- (void)__initMutex:(pthread_mutex_t *)mutex
{
// 递归锁:允许同一个线程对一把锁进行重复加锁
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
}
可以对同一线程,重复加锁。不同线程不能重复加锁。
/**
线程1:otherTest(+-)
otherTest(+-)
otherTest(+-)
线程2:otherTest(等待)
*/
OSSpinkLock 和 pthread_mutex 汇编分析
Xcode --> Debug --> Debug Workflow -- Always show Disassembly 显示汇编代码后,这时想要单步,不能用Xcode上的单步按钮,而是用LLDB命令。
step简写s是单步,但是是单步代码的,会一下过掉好几行汇编指令,stepinstruction简写stepi或者si是单步一条汇编的。nexti也是过掉一条汇编,但是遇到方法不会进入方法,直接过了方法。
改造下卖票代码,10个线程同时卖票
- (void)ticketTest
{
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
}
}
OSSpinkLock
- (void)__saleTicket
{
OSSpinLockLock(&_ticketLock);
[super __saleTicket];
OSSpinLockUnlock(&_ticketLock);
}
断点断在 OSSpinLockLock(&_ticketLock);,第一次断在这时,是第一个线程,然后放开。第二次断在这儿时,进入显示汇编模式,然后 si,...
到callq(调方法),进入OSSpinLockLock,si进入,继续几个si...,然后进入_OSSpinLockLockSlow,继续si...
会一直在 0x7fff518c58d1 和 0x7fff518c58e6中间,jne j是jump跳入,ne是条件判断,很明显这部分代码是 while循环。所以OSSpinkLock是自旋锁,会一直占用CPU资源。
pthread_mutex
同理一直si,会进入_pthread_mutex_firstfit_lock_wait
然后N个si后进入 __psynch_mutexwait,
最后看到 syscall指令,这是系统调用,这之后就没反应了,线程休眠了。
所以 pthread_mutex是互斥锁。
os_unfair_lock
最后也是,syscall指令后,没反应了。线程休眠了。所以os_unfair_lock不是自旋锁。点进去看,头文件注释:
* @abstract
* Low-level lock that allows waiters to block efficiently on contention.
Low-level 简称 LL Lock或LLL,低级锁。Low-level lock的特点等不到锁就休眠。自旋锁OSSpinkLock是高级锁。