多个线程访问同一块资源的时候,很容易引发数据混乱问题。
看下面的例子,3个线程分别买票7次,看下面打印结果
- (void)ticketTest{
self.ticketsCount = 21;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 3; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 7; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
NSLog(@"当前剩余票数-> %d", oldMoney);
}
输出:
2018-12-05 10:01:18.630619+0800 001--Block[7762:559583] 当前剩余票数-> 20
2018-12-05 10:01:18.630619+0800 001--Block[7762:559582] 当前剩余票数-> 20
2018-12-05 10:01:18.630660+0800 001--Block[7762:559581] 当前剩余票数-> 20
2018-12-05 10:01:18.630853+0800 001--Block[7762:559583] 当前剩余票数-> 19
2018-12-05 10:01:18.630853+0800 001--Block[7762:559582] 当前剩余票数-> 19
2018-12-05 10:01:18.631212+0800 001--Block[7762:559581] 当前剩余票数-> 18
2018-12-05 10:01:18.631220+0800 001--Block[7762:559583] 当前剩余票数-> 17
2018-12-05 10:01:18.631491+0800 001--Block[7762:559582] 当前剩余票数-> 16
2018-12-05 10:01:18.631606+0800 001--Block[7762:559581] 当前剩余票数-> 15
2018-12-05 10:01:18.632321+0800 001--Block[7762:559583] 当前剩余票数-> 14
2018-12-05 10:01:18.632695+0800 001--Block[7762:559582] 当前剩余票数-> 13
2018-12-05 10:01:18.633174+0800 001--Block[7762:559581] 当前剩余票数-> 12
2018-12-05 10:01:18.633746+0800 001--Block[7762:559583] 当前剩余票数-> 11
2018-12-05 10:01:18.634030+0800 001--Block[7762:559582] 当前剩余票数-> 10
2018-12-05 10:01:18.634240+0800 001--Block[7762:559581] 当前剩余票数-> 9
2018-12-05 10:01:18.635149+0800 001--Block[7762:559583] 当前剩余票数-> 8
2018-12-05 10:01:18.635715+0800 001--Block[7762:559582] 当前剩余票数-> 7
2018-12-05 10:01:18.636263+0800 001--Block[7762:559581] 当前剩余票数-> 6
2018-12-05 10:01:18.637100+0800 001--Block[7762:559583] 当前剩余票数-> 5
2018-12-05 10:01:18.637324+0800 001--Block[7762:559582] 当前剩余票数-> 4
2018-12-05 10:01:18.637685+0800 001--Block[7762:559581] 当前剩余票数-> 3
正常情况下我有21张票,然后卖了21次,剩余票数应该是0,但是打印结果竟然是3,所以这里就存在了线程安全问题。
出现线程安全的原因就是在同一个时间,多个线程同时读取一个值,像线程A和B同时读取了当前票数为10,等于是卖了两张票,但是总票数其实就减少了一张。
解决方法
线程同步方法,加锁。
两种锁的加锁原理
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。
自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
对比 互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
两种锁的应用
互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
1 临界区有IO操作
2 临界区代码复杂或者循环量大
3 临界区竞争非常激烈
4 单核处理器
至于自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下,自旋锁一般用于多核的服务器。
1、OSSpinLock自旋锁的使用
OSSpinLock叫做”自旋锁”,使用时需要导入头文件#import <libkern/OSAtomic.h>
// OSSpinLock自旋锁的初始化
OSSpinLock _lock = OS_SPINLOCK_INIT;
// 锁定
OSSpinLockLock(&_lock);
// 解锁
OSSpinLockUnlock(&_lock);
这样我们就可以把上面买票代码加锁了
- (void)ticketTest{
_ticketLock = OS_SPINLOCK_INIT;
self.ticketsCount = 21;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (NSInteger i = 0; i < 3; i++) {
dispatch_async(queue, ^{
for (int i = 0; i < 7; i++) {
[self sellingTickets];
}
});
}
}
//卖票
- (void)sellingTickets{
OSSpinLockLock(&_ticketLock);
int oldMoney = self.ticketsCount;
sleep(.2);
oldMoney -= 1;
self.ticketsCount = oldMoney;
OSSpinLockUnlock(&_ticketLock);
NSLog(@"当前剩余票数-> %d", oldMoney);
}
这样就可以保证票数卖完,数量同步了
OSSpinLock
在iOS10.0以后就被弃用了,可以使用os_unfair_lock_lock
替代。而且还有一些安全性问题,具体参考不再安全的 OSSpinLock
2、os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等 需要导入头文件#import <os/lock.h>
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
3、pthread_mutex
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。需要导入头文件#import <pthread.h> 使用步骤
4、NSLock
NSLock是对mutex普通锁的封装。pthread_mutex_init(mutex, NULL);
NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO
#import "LockDemo.h"
@interface LockDemo()
@property (strong, nonatomic) NSLock *ticketLock;
@end
@implementation LockDemo
//卖票
- (void)sellingTickets{
[self.ticketLock lock];
[super sellingTickets];
[self.ticketLock unlock];
}
@end
网友评论