1. 多线程安全隐患
在多线程开发时,如果遇到多个线程同时访问同一块资源,就会产生安全隐患的问题。多个线程访问同一个文件,同一个变量或者是同一个对象,最终会导致数据的错乱等问题
1.1 举例一:存取钱问题
假设我们可以同时进行存钱和取钱操作,存钱和取钱操作就会同时访问我们的账户。具体场景如下:
- 假设银行卡的余额有500块钱
- 此时我们进行取钱操作,读取银行卡的余额为500元,在取钱操作没有完成的同一时间,我们进行存钱操作,此时读取的银行卡余额为500元
- 取出50元,500-50 =450元,此时更新账户余额为450元
- 同时存入100元,500+100=600元,更新账户余额为600元
- 最终,余额变为600元,正常的逻辑应该是500-50+100=550元,产生了安全隐患。
1.1.1 代码示例
@interface ViewController ()
@property (nonatomic, assign) int money;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self executeMoneyTask];
}
- (void)executeMoneyTask {
self.money = 500;
// 开启2条线程,同时进行存钱和取钱的操作
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self drawMoney];
}
});
}
/// 存钱操作
- (void)saveMoney {
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
}
/// 取钱操作
- (void)drawMoney {
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
}
打印结果:
取了50块,余额550, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额600,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额500, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额600,<NSThread: 0x600003609000>{number = 6, name = (null)}
存了100块,余额700,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额650, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额750,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额700, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
存了100块,余额800,<NSThread: 0x600003609000>{number = 6, name = (null)}
取了50块,余额750, <NSThread: 0x60000360ba40>{number = 7, name = (null)}
1.1.2 打印结果分析
- 上面代码分别在2个线程同时执行存钱和取钱的操作,取钱和存钱操作分别执行了5次。
- 从打印结果分析,虽然最终的余额是750元,但是中间确实出现了余额显示不正确的问题。
1.2 举例二:卖票问题
假设线上去我们去购买郭德纲相声专场的票,一共开设2个售票窗口,同时进行卖票。具体场景如下:
- 1号窗口读取票的剩余1000张,同时2号窗口读取票的剩余1000张
- 1号窗口卖出1张剩余1000-1=999张,2号窗口卖出1张1000-1=999张
- 随着时间的推移,数据就会非常的混乱了,结果你会发现卖出的票数远远大于剧场可以承载的观众人数。
1.2.1 代码示例
@interface ViewController ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) int tickets;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// [self executeMoneyTask];
[self executeTicketTask];
}
#pragma mark - 买票问题
- (void)executeTicketTask {
self.tickets = 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];
}
});
}
- (void)saleTicket {
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
}
打印结果:
还剩14张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩12张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩13张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩11张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩11张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩10张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩9张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩8张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩6张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
还剩7张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩5张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩4张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩3张票,<NSThread: 0x600003dff780>{number = 5, name = (null)}
还剩2张票,<NSThread: 0x600003dffc80>{number = 4, name = (null)}
还剩1张票,<NSThread: 0x600003de9640>{number = 6, name = (null)}
1.2.2 打印结果分析
- 开启3条线程,同时进行买票操作,正常情况下,打印结果剩余应该是0张票
- 由于同时共享一块资源导致数据发生了错乱,这是非常危险的,接下来就研究一下如何解决以上两个问题。
2. 线程同步技术
上面出现的问题,在开发中会产生很严重的数据安全问题,通常解决方案就是线程同步技术。线程同步技术最常见的就是加锁。即在操作数据之前给线程加一把锁,数据操作结束后解锁。在有锁时候,其他线程是无法访问这块资源的,只要当这把锁被解开了,其他线程才可以进入来操作数据,其他线程进入之前,同样再加锁。这样一系列操作之后,就可以保证数据的安全了。
2.1 iOS中的线程同步方案
OSSpinLockos_unfair_lockpthread_mutexdispatch_semaphoredispath_queue(DISPATCH_QUEUE_SERIAL)NSLockNSRecursiveLockNSConditionNSConditionLock@synchronized
2.1.1 OSSpinLock
- OSSpinLock叫做自旋锁,等待锁的线程会处于忙等状态,一直占用着CPU的资源
- 目前已经被弃用,原因是不再安全,可能会出现优先级反转问题,即如果等待锁的线程优先级比较高,它会一直占用着CPU的资源,优先级低的线程就无法释放锁
- 导入
#import <libkern/OSAtomic.h> - 解决卖票问题
- (void)saleTicket {
// 初始化1次,OS_SPINLOCK_INIT 值就是0(进入头文件可以看到)
static OSSpinLock lock = OS_SPINLOCK_INIT;
// 加锁操作
OSSpinLockLock(&lock);
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
// 解锁操作
OSSpinLockUnlock(&lock);
}
打印结果:
还剩14张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩13张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩12张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩11张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩10张票,<NSThread: 0x600000f254c0>{number = 4, name = (null)}
还剩9张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩8张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩7张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩6张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩5张票,<NSThread: 0x600000f24940>{number = 5, name = (null)}
还剩4张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩3张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩2张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩1张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
还剩0张票,<NSThread: 0x600000f28240>{number = 6, name = (null)}
注意:
首先要保证锁只有1把,只有是同一把锁才能保证线程安全。
static OSSpinLock lock = OS_SPINLOCK_INIT;初始化一把锁,为什么可以使用static修饰,因为static修饰的变量是在编译时期确定值的,而OS_SPINLOCK_INIT本质就是一个常量值0。(具体可以进入头文件看一下这个宏定义)
- 解决存取钱问题
/// 存钱操作
- (void)saveMoney {
OSSpinLockLock(&_moneyLock);
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_moneyLock);
}
/// 取钱操作
- (void)drawMoney {
OSSpinLockLock(&_moneyLock);
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
OSSpinLockUnlock(&_moneyLock);
}
打印结果:
存了100块,余额600,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额700,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额800,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额900,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
存了100块,余额1000,<NSThread: 0x600003dc16c0>{number = 6, name = (null)}
取了50块,余额950, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额900, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额850, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额800, <NSThread: 0x600003dae880>{number = 5, name = (null)}
取了50块,余额750, <NSThread: 0x600003dae880>{number = 5, name = (null)}
- 存钱和取钱操作都需要加锁,而且必须是同一把锁
2.1.2 os_unfair_lock
因为OSSpinLock存在问题,所以在iOS10就已经被废弃了,在我们编写代码的时候,Xcode其实会提示我们使用os_unfair_lock进行替代。
os_unfair_lock 与 OSSpinLock不同之处在于,等待os_unfair_lock锁的线程会处于休眠状态。
- 在使用os_unfair_lock需要导入头文件
import <os/lock.h>
使用的方式和OSSpinLock很类似,直接看代码
@interface ViewController ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) int tickets;
@property (nonatomic, assign) os_unfair_lock moneyLock;
@property (nonatomic, assign) os_unfair_lock ticketLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化锁
self.moneyLock = OS_UNFAIR_LOCK_INIT;
self.ticketLock = OS_UNFAIR_LOCK_INIT;
[self executeMoneyTask];
[self executeTicketTask];
}
- (void)saleTicket {
// 加锁操作
os_unfair_lock_lock(&_ticketLock);
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
// 解锁操作
os_unfair_lock_unlock(&_ticketLock);
}
/// 存钱操作
- (void)saveMoney {
// 加锁操作
os_unfair_lock_lock(&_moneyLock);
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
// 解锁操作
os_unfair_lock_unlock(&_moneyLock);
}
/// 取钱操作
- (void)drawMoney {
// 加锁操作
os_unfair_lock_lock(&_moneyLock);
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
// 解锁操作
os_unfair_lock_unlock(&_moneyLock);
}
2.1.3 pthread_mutex
- 使用pthread_mutex需要导入头文件
#import <pthread.h> - pthread_mutex作为互斥锁,等待锁的线程会处于休眠的状态,可以解决上面的卖票和取钱的问题
#import <pthread.h>
@interface ViewController ()
@property (nonatomic, assign) int money;
@property (nonatomic, assign) int tickets;
@property (nonatomic, assign) pthread_mutex_t moneyMutex;
@property (nonatomic, assign) pthread_mutex_t ticketMutex;
@end
- (void)viewDidLoad {
[super viewDidLoad];
// 执行存取钱任务
[self executeMoneyTask];
// 执行卖票任务
[self executeTicketTask];
pthread_mutexattr_t attr;
// 初始化锁属性
pthread_mutexattr_init(&attr);
// 设置锁属性类型,互斥锁
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_init(&_moneyMutex, &attr);
pthread_mutex_init(&_ticketMutex, &attr);
// 销毁锁属性
pthread_mutexattr_destroy(&attr);
}
// 卖票
- (void)saleTicket {
// 加锁操作
pthread_mutex_lock(&_ticketMutex);
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
// 解锁操作
pthread_mutex_unlock(&_ticketMutex);
}
/// 存钱操作
- (void)saveMoney {
pthread_mutex_lock(&_moneyMutex);
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
pthread_mutex_unlock(&_moneyMutex);
}
/// 取钱操作
- (void)drawMoney {
pthread_mutex_lock(&_moneyMutex);
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
pthread_mutex_unlock(&_moneyMutex);
}
// 销毁锁
- (void)dealloc {
pthread_mutex_destroy(&_moneyMutex);
pthread_mutex_destroy(&_ticketMutex);
}
- 在上面的代码中,可以通过设置锁属性,来设置锁的类型
/*
* Mutex type attributes
*/
#define PTHREAD_MUTEX_NORMAL 0 // 互斥锁
#define PTHREAD_MUTEX_ERRORCHECK 1 // 错误检查(不用管)
#define PTHREAD_MUTEX_RECURSIVE 2 // 递归锁
#define PTHREAD_MUTEX_DEFAULT PTHREAD_MUTEX_NORMAL
- 设置
PTHREAD_MUTEX_RECURSIVE锁属性,可以解决递归调用时,死锁的问题,递归锁进行多次加锁的操作 - test1方法进行递归调用,如果使用互斥锁,打印只打印1次,因为第1次调用进行了加锁的操作,当递归调用test1方法,发现已经加锁了,所以不会再继续执行,需要等待解锁操作,所以导致了死锁。如何解决这个问题呢?
- 使用递归锁,设置锁类型的时候,设置为PTHREAD_MUTEX_RECURSIVE即可。
- (void)viewDidLoad {
[super viewDidLoad];
pthread_mutexattr_t attr;
// 初始化锁属性
pthread_mutexattr_init(&attr);
// 设置锁属性类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_init(&_mutex, &attr);
pthread_mutexattr_destroy(&attr);
[self test1];
}
- (void)test1 {
pthread_mutex_lock(&_mutex);
NSLog(@"test1");
static int count = 0;
if (count < 10) {
count++;
[self test1];
}
pthread_mutex_unlock(&_mutex);
}
2.1.4 pthread_mutex 条件的使用
-
需求:
- 有2个线程,线程A对数组进行元素添加,线程B对数组中的元素进行删除。
- 如果数组中没有元素,就等待线程A添加元素后再进行删除操作
-
需求分析:
- 删除操作和添加操作在2个线程执行,执行的顺序不确定
- 要访问同一份资源(可变数组),所以操作前后要进行加锁操作保证线程安全
根据上面的需求和分析,我们就可以运用到pthread中条件的使用
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *data;
@property (nonatomic, assign) pthread_mutex_t mutex;
@property (nonatomic, assign) pthread_cond_t conditon;
@end
- (void)viewDidLoad {
[super viewDidLoad];
pthread_mutexattr_t attr;
// 初始化锁属性
pthread_mutexattr_init(&attr);
// 设置锁属性类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
// 初始化锁
pthread_mutex_init(&_mutex, &attr);
// 初始化条件
pthread_cond_init(&_conditon, NULL);
[self test1];
}
- (void)test1 {
[[[NSThread alloc] initWithTarget:self selector:@selector(removeObject) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(addObject) object:nil] start];
}
- (void)addObject {
pthread_mutex_lock(&_mutex);
NSLog(@"添加元素");
[self.data addObject:@"test"];
pthread_cond_signal(&_conditon);
pthread_mutex_unlock(&_mutex);
}
- (void)removeObject {
pthread_mutex_lock(&_mutex);
NSLog(@"remove - begin");
if (self.data.count == 0) {
// 如果数组中没有任何元素,等待数组添加元素
pthread_cond_wait(&_conditon, &_mutex);
}
NSLog(@"删除元素");
[self.data removeLastObject];
pthread_mutex_unlock(&_mutex);
}
- 当删除操作先执行的时候,先加锁,如果数组中没有任何元素,此时使用
pthread_cond_wait(&_conditon, &_mutex);进行等待操作,此时同时放开mutex锁。 - 添加操作加锁,添加元素,发送信号,解锁操作
- 删除操作线程接收到信号,进行加锁执行删除操作,删除结束后,解锁。
2.1.5 NSLock
- NSLock是对mutex普通锁的封装,遵守了NSLocking协议,提供了lock和unlock方法进行加锁和解锁的操作
- 下面是使用NSLock,对线程安全的处理
/// 存钱操作
- (void)saveMoney {
[self.moneyLock lock];
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
[self.moneyLock unlock];
}
/// 取钱操作
- (void)drawMoney {
[self.moneyLock lock];
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
[self.moneyLock unlock];
}
- (NSLock *)moneyLock {
if (_moneyLock == nil) {
_moneyLock = [[NSLock alloc] init];
}
return _moneyLock;
}
- (void)saleTicket {
[self.ticketLock lock];
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
[self.ticketLock unlock];
}
- (NSLock *)ticketLock {
if (_ticketLock == nil) {
_ticketLock = [[NSLock alloc] init];
}
return _ticketLock;
}
2.1.6 NSRecursiveLock
NSRecursiveLock是对mutex的封装,封装的是自旋锁。操作的方式与NSLock类似,有lock和unlock的操作。
- (void)viewDidLoad {
[super viewDidLoad];
[self test1];
}
- (void)test1 {
[self.lock lock];
NSLog(@"test1");
static int count = 0;
if (count < 10) {
count++;
[self test1];
}
[self.lock unlock];
}
- (NSRecursiveLock *)lock {
if (_lock == nil) {
_lock = [[NSRecursiveLock alloc] init];
}
return _lock;
}
2.1.7 NSCondition
- NSConditon 是对pthread_cond的封装,使条件操作变得更加面向对象
- (void)addObject {
[self.condition lock];
NSLog(@"添加元素");
[self.data addObject:@"test"];
[self.condition signal];
[self.condition unlock];
}
- (void)removeObject {
[self.condition lock];
NSLog(@"remove - begin");
if (self.data.count == 0) {
// 如果数组中没有任何元素,等待数组添加元素
[self.condition wait];
}
NSLog(@"删除元素");
[self.data removeLastObject];
[self.condition unlock];
}
2.1.8 NSConditonLock
- 需求:分别在3个线程执行任务,要求任务1 任务2 任务3 依次执行。(相互依赖执行)
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化NSConditionLock对象
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
[self test1];
}
// 需求:任务1 任务2 任务3 依次按顺序执行
- (void)test1 {
[[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start];
}
- (void)task1 {
[self.conditionLock lockWhenCondition:1];
NSLog(@"执行任务1 - %@", [NSThread currentThread]);
[self.conditionLock unlockWithCondition:2];
}
- (void)task2 {
[self.conditionLock lockWhenCondition:2];
NSLog(@"执行任务2 - %@", [NSThread currentThread]);
[self.conditionLock unlockWithCondition:3];
}
- (void)task3 {
[self.conditionLock lockWhenCondition:3];
NSLog(@"执行任务3 - %@", [NSThread currentThread]);
[self.conditionLock unlock];
}
打印结果:
执行任务1 - <NSThread: 0x600003c0b340>{number = 5, name = (null)}
执行任务2 - <NSThread: 0x600003c08900>{number = 6, name = (null)}
执行任务3 - <NSThread: 0x600003c09180>{number = 7, name = (null)}
- 初始化NSConditonLock对象,并设置条件为1。
- 当条件为1时,任务1才会执行,并加锁,当任务1执行完毕,解锁,将条件设置为2
- 任务2想要执行,必须当条件为2时才可以,否则会一直等待,当发现条件为2,执行任务2,任务2执行结束,解锁将条件设置为3
- 任务3发现条件为3,加锁操作,执行任务3,如果任务3之后没有其他任务需要依赖任务3的执行结果,那么就可以直接解锁。
注意:
- 在初始化NSConditonLock对象时,如果不传入条件
[NSConditionLock alloc] init],默认的条件就是0。- 如果加锁操作,不传入条件,那么不会根据条件来进行加锁操作,同理 ,如果解锁操作不设置任何条件,直接解锁。
2.1.9 使用串行队列,实现线程同步
- 使用串行队列,同样可以实现线程的同步,只要保证不在同一时间读写同一块资源即可。
- (void)viewDidLoad {
[super viewDidLoad];
// 创建串行队列
self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
[self executeMoneyTask];
[self executeTicketTask];
}
/// 卖票
- (void)saleTicket {
dispatch_sync(self.serialQueue, ^{
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
});
}
/// 存钱操作
- (void)saveMoney {
dispatch_sync(self.serialQueue, ^{
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
});
}
/// 取钱操作
- (void)drawMoney {
dispatch_sync(self.serialQueue, ^{
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
});
}
2.1.10 semaphore信号量实现线程同步
- semaphore可以控制线程的最大并发数
- 假设我们需要创建20条线程去执行任务,设置semaphore的值为5,就可以控制最多只有5条线程在执行任务
dispatch_semaphore_wait函数会对semaphore的值进行判断- 如果semaphore的值 > 0,就让semaphore的值减1,继续执行下面的代码
- 如果semaphore的值 <= 0,就会休眠等待,直到semaphore的值>0,继续让semaphore的值减1,继续执行下面的代码
dispatch_semaphore_signal会使semaphore的值加1- 下面的代码打印的效果就是,每隔2s进行5次打印。
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化信号量,并设置信号量的值为5
self.semaphore = dispatch_semaphore_create(5);
[self test];
}
- (void)test {
// 创建20个线程
for (int i = 0; i < 20; i++) {
[[[NSThread alloc] initWithTarget:self selector:@selector(executeTask) object:nil] start];
}
}
- (void)executeTask {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"%@", [NSThread currentThread]);
dispatch_semaphore_signal(self.semaphore);
}
- 上面的代码展示了使用semaphore可以控制最大并发数量,那么如何利用semaphore实现线程同步呢?
- 其实只需要设置semaphore的初始值为1,就可以了,同时最多只有1条线程执行任务,通过设置semaphore的值加1和减1,进而控制同一时间只能有一个线程执行任务,最终实现线程同步。
- (void)viewDidLoad {
[super viewDidLoad];
self.moneySemaphore = dispatch_semaphore_create(1);
self.ticketSemaphore = dispatch_semaphore_create(1);
[self executeMoneyTask];
[self executeTicketTask];
}
- (void)saleTicket {
dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
dispatch_semaphore_signal(self.ticketSemaphore);
}
/// 存钱操作
- (void)saveMoney {
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
dispatch_semaphore_signal(self.moneySemaphore);
}
/// 取钱操作
- (void)drawMoney {
dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
dispatch_semaphore_signal(self.moneySemaphore);
}
2.1.11 @synchronized
- 使用
@synchronized(obj)就可以实现加锁的操作 @synchronized会根据传入obj对象,来获取一把锁,只要保证obj对象是同一个对象,就可以保证是同一把锁,代码块的左侧大括号开始就进行加锁操作,直到操作结束,到右侧大括号解锁操作。@synchronized性能很低,在开发中不推荐使用
- (void)saleTicket {
@synchronized (self) {
int oldTickets = self.tickets;
oldTickets -= 1;
self.tickets = oldTickets;
NSLog(@"还剩%d张票,%@", self.tickets, [NSThread currentThread]);
}
}
/// 存钱操作
- (void)saveMoney {
@synchronized (self) {
int oldMoney = self.money;
oldMoney += 100;
self.money = oldMoney;
NSLog(@"存了100块,余额%d,%@", self.money, [NSThread currentThread]);
}
}
/// 取钱操作
- (void)drawMoney {
@synchronized (self) {
int oldMoney = self.money;
oldMoney -= 50;
self.money = oldMoney;
NSLog(@"取了50块,余额%d, %@", self.money, [NSThread currentThread]);
}
}
注意:
@synchronized() 传入参数,不一定非得是self,只要保证是同一个对象即可。