目录
一、数据竞争问题
二、线程同步方案
1、加锁
1.1 自旋锁——OSSpinLock
1.2os_unfair_lock
1.3 互斥锁——pthread_mutex
普通锁、NSLock
1.4 递归锁——pthread_mutex
递归锁、NSRecursiveLock
、@synchronized
2、设置线程的最大并发数为1
2.1 用GCD信号量设置
2.2 把多个线程放入GCD串行队列,让它们一个一个串行执行
3、线程同步方案性能对比
一、数据竞争问题
多个线程并发执行,同一时间访问同一块数据(如同一个变量、对象、文件等)、且其中至少有一个线程是在对数据进行写操作(所有的线程都是读取的话就没事),就会出现数据竞争,从而导致数据错乱。
因为多个线程是并发执行的,我们无法确定它们到底是在哪一时刻访问这块数据,比如线程1正在修改这块数据的值,修改完之前,线程2又来修改这块数据的值了(即出现了数据竞争),那这块数据的值最终到底是被谁修改的我们无法确定,而且这块数据的值很有可能跟我们直观上以为的不一样(即导致了数据错乱)。
我们举“卖票”的例子来演示一下数据竞争导致数据错乱的问题。
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount; // 总票数
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 假设就剩10张票了
self.totalTicketCount = 10;
// 假设有10个窗口在卖票 --> 即多个线程
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 卖一张票
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
}
@end
// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 5, name = (null)}
剩余票数:7,{number = 7, name = (null)}
剩余票数:6,{number = 8, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 7, name = (null)}
剩余票数:1,{number = 8, name = (null)}
剩余票数:4,{number = 5, name = (null)}
剩余票数:5,{number = 4, name = (null)}
剩余票数:0,{number = 3, name = (null)}
我们直观上以为十条数据应该是“9、8、7、6、5、4、3、2、1、0”这样倒序排下来的,但从第五条数据起就全错了,这就是因为出现了数据竞争而导致了数据错乱。
二、线程同步方案
如何解决数据竞争呢?既然数据竞争是因为“多个线程并发执行,同一时间访问同一块数据”才出现的,而我们肯定还是要让“多个线程并发执行”(你当然可以直接不使用多线程技术,但那不就降低任务的执行效率了嘛),所以就只能从后半句下手。
同一时间只让多个线程中的一个线程访问这块数据,一个线程访问完再让下一个线程访问不就解决了嘛,这其实就是我们常说的线程同步,即线程一个接一个地挨个执行,而常用的线程同步方案有两种:加锁和设置线程的最大并发数为1。
1、加锁
加锁是指多个线程使用同一把锁,对访问数据的关键代码进行加锁。这样每个线程在访问数据之前都会看看锁是否锁上了,如果锁上了,线程就阻塞在锁这里等待(正是这个阻塞才达到了线程同步的效果),直到锁解开后才会继续执行,如果没锁上,就加锁并访问数据。
不能每个线程都使用一把新锁,也最好不要无脑地对很多无关代码进行加锁。iOS常用的锁有:
1.1 自旋锁——OSSpinLock
自旋锁是指“如果线程发现锁已经锁上了,就阻塞在锁这里等待”的这个“等待”是忙等(
busy-wait
),即线程会一直占用CPU资源来判断锁是否解开,相当于在这里写了个while
循环。所以如果加锁代码是轻量级的数据访问(比如
int
数据简单的+1/-1操作,系统引用计数表那里不就使用的是自旋锁嘛),完全可以使用自旋锁,因为这种情况下互斥锁反复地休眠和唤醒反而更耗性能,还不如让线程忙等一小会儿呢。自旋锁在iOS10之后就被废弃了,因为它可能出现优先级反转问题,不是那么安全。比如低优先级的线程访问数据时加了锁,高优先级的线程第二次访问数据时CPU就会优先分配资源让它忙等,这样CPU就没有机会分配资源给低优先级的线程去访问数据和解锁了,这就进而导致高优先级的线程一直处于忙等,这就造成了死锁。苹果推荐使用
os_unfair_lock
替代它。
#import "ViewController.h"
#import // 导入头文件
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, assign) OSSpinLock spinLock; // 锁
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建锁
self.spinLock = OS_SPINLOCK_INIT;
self.totalTicketCount = 10;
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 加锁
OSSpinLockLock(&_spinLock);
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_spinLock);
}
@end
// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 6, name = (null)}
剩余票数:7,{number = 6, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 6, name = (null)}
剩余票数:4,{number = 6, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 5, name = (null)}
剩余票数:1,{number = 7, name = (null)}
剩余票数:0,{number = 4, name = (null)}
1.2 os_unfair_lock
- 苹果推荐使用
os_unfair_lock
替代自旋锁,不过要iOS10之后才能用。os_unfair_lock
其实也是一个互斥锁,因为它等待上一个线程解锁时也是休眠,而不是忙等(busy-wait
)。
#import "ViewController.h"
#import // 导入头文件
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, assign) os_unfair_lock unfairLock; // 锁
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建锁
self.unfairLock = OS_UNFAIR_LOCK_INIT;
self.totalTicketCount = 10;
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 加锁
os_unfair_lock_lock(&_unfairLock);
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
// 解锁
os_unfair_lock_unlock(&_unfairLock);
}
@end
// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 3, name = (null)}
剩余票数:7,{number = 5, name = (null)}
剩余票数:6,{number = 7, name = (null)}
剩余票数:5,{number = 4, name = (null)}
剩余票数:4,{number = 8, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 9, name = (null)}
剩余票数:1,{number = 10, name = (null)}
剩余票数:0,{number = 11, name = (null)}
1.3 互斥锁——pthread_mutex
普通锁、NSLock
互斥锁是指“如果线程发现锁已经锁上了,就阻塞在锁这里等待”的这个“等待”是休眠,而不是忙等(
busy-wait
),即线程会进入休眠状态,而不会一直占用CPU资源来判断锁是否解开,锁解开后系统会唤醒该线程。所以如果加锁代码的任务量较大,使用互斥锁就比较划算,因为这种情况下自旋锁的忙等会很耗性能。
NSLock
是对pthread_mutex
普通锁的封装。
-
pthread_mutex
普通锁
#import "ViewController.h"
#import // 导入头文件
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, assign) pthread_mutex_t mutexLock; // 锁
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); // pthread_mutex普通锁
// 创建锁
pthread_mutex_init(&_mutexLock, &attr);
self.totalTicketCount = 10;
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 加锁
pthread_mutex_lock(&_mutexLock);
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
// 解锁
pthread_mutex_unlock(&_mutexLock);
}
- (void)dealloc {
// 销毁锁
pthread_mutex_destroy(&_mutexLock);
}
@end
// 控制台打印:
剩余票数:9,{number = 6, name = (null)}
剩余票数:8,{number = 6, name = (null)}
剩余票数:7,{number = 7, name = (null)}
剩余票数:6,{number = 7, name = (null)}
剩余票数:5,{number = 7, name = (null)}
剩余票数:4,{number = 3, name = (null)}
剩余票数:3,{number = 6, name = (null)}
剩余票数:2,{number = 5, name = (null)}
剩余票数:1,{number = 8, name = (null)}
剩余票数:0,{number = 4, name = (null)}
NSLock
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, strong) NSLock *lock; // 锁
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建锁
self.lock = [[NSLock alloc] init];
self.totalTicketCount = 10;
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 加锁
[self.lock lock];
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
// 解锁
[self.lock unlock];
}
@end
// 控制台打印:
剩余票数:9,{number = 5, name = (null)}
剩余票数:8,{number = 5, name = (null)}
剩余票数:7,{number = 6, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 6, name = (null)}
剩余票数:4,{number = 7, name = (null)}
剩余票数:3,{number = 8, name = (null)}
剩余票数:2,{number = 5, name = (null)}
剩余票数:1,{number = 4, name = (null)}
剩余票数:0,{number = 3, name = (null)}
1.4 递归锁——pthread_mutex
递归锁、NSRecursiveLock
、@synchronized
- 递归锁是指允许同一个线程对同一把锁重复加锁,但其它的线程看到上锁时该等还是得等,它可以加锁存在递归调用的代码,当然也可以加锁没有递归调用的代码。所有的递归锁都是互斥锁,它只是互斥锁分支出来可以加锁递归调用的锁,我们可以理解为递归锁继承于互斥锁。
NSRecursiveLock
和@synchronized
都是对pthread_mutex
递归锁的封装。
-
pthread_mutex
递归锁
#import "ViewController.h"
#import // 导入头文件
@interface ViewController ()
@property (nonatomic, assign) pthread_mutex_t mutexLock; // 锁
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); // pthread_mutex递归锁
// 创建锁
pthread_mutex_init(&_mutexLock, &attr);
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test];
});
}
}
- (void)test {
// 加锁
pthread_mutex_lock(&_mutexLock);
NSLog(@"%s,%@", __func__, [NSThread currentThread]);
static int count = 0;
if (count == 0) {
count ++;
[self test]; // 递归调用
}
// 解锁
pthread_mutex_unlock(&_mutexLock);
}
- (void)dealloc {
// 销毁锁
pthread_mutex_destroy(&_mutexLock);
}
@end
// 控制台打印:共打印11次,前2次是第一条线程进入时递归调用打印的,后面的线程都不会再递归调用
-[ViewController test],{number = 5, name = (null)}
-[ViewController test],{number = 5, name = (null)}
-[ViewController test],{number = 3, name = (null)}
-[ViewController test],{number = 7, name = (null)}
-[ViewController test],{number = 4, name = (null)}
-[ViewController test],{number = 8, name = (null)}
-[ViewController test],{number = 9, name = (null)}
-[ViewController test],{number = 10, name = (null)}
-[ViewController test],{number = 11, name = (null)}
-[ViewController test],{number = 12, name = (null)}
-[ViewController test],{number = 13, name = (null)}
NSRecursiveLock
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSRecursiveLock *recursiveLock; // 锁
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建锁
self.recursiveLock = [[NSRecursiveLock alloc] init];
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self test];
});
}
}
- (void)test {
// 加锁
[self.recursiveLock lock];
NSLog(@"%s,%@", __func__, [NSThread currentThread]);
static int count = 0;
if (count == 0) {
count ++;
[self test]; // 递归调用
}
// 解锁
[self.recursiveLock unlock];
}
@end
// 控制台打印:共打印11次,前2次是第一条线程进入时递归调用打印的,后面的线程都不会再递归调用
-[ViewController test],{number = 7, name = (null)}
-[ViewController test],{number = 7, name = (null)}
-[ViewController test],{number = 6, name = (null)}
-[ViewController test],{number = 5, name = (null)}
-[ViewController test],{number = 3, name = (null)}
-[ViewController test],{number = 4, name = (null)}
-[ViewController test],{number = 8, name = (null)}
-[ViewController test],{number = 9, name = (null)}
-[ViewController test],{number = 10, name = (null)}
-[ViewController test],{number = 11, name = (null)}
-[ViewController test],{number = 12, name = (null)}
@synchronized
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.totalTicketCount = 10;
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 加锁、解锁
@synchronized (self) {
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
}
}
@end
// 控制台打印:
剩余票数:9,{number = 5, name = (null)}
剩余票数:8,{number = 4, name = (null)}
剩余票数:7,{number = 6, name = (null)}
剩余票数:6,{number = 7, name = (null)}
剩余票数:5,{number = 8, name = (null)}
剩余票数:4,{number = 9, name = (null)}
剩余票数:3,{number = 10, name = (null)}
剩余票数:2,{number = 11, name = (null)}
剩余票数:1,{number = 12, name = (null)}
剩余票数:0,{number = 13, name = (null)}
2、设置线程的最大并发数为1
2.1 用GCD信号量设置
- 虽然有多个线程,但是我们设置线程的最大并发数为1,不就实现线程同步了嘛。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@property (strong, nonatomic) dispatch_semaphore_t semaphore;// 信号量
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建信号量,并设置信号量的初始值为1,即设置线程的最大并发数为1
self.semaphore = dispatch_semaphore_create(1);
self.totalTicketCount = 10;
for (int i = 0; i < 10; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self saleTicket];
});
}
}
- (void)saleTicket {
// 每个线程进来执行到dispatch_semaphore_wait函数时,函数内部有如下操作:
// 如果发现信号量的值 > 0,就让信号量的值 - 1,并让这条线程往下执行代码
// 如果发现信号量的值 <= 0,就会让线程阻塞在这里休眠等待,直到信号量的值 > 0,才再次让信号量的值 - 1,并唤醒一条休眠的线程进去执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
// 每个线程执行完有效代码后,执行到dispatch_semaphore_signal函数时,函数内部有如下操作:
// 会让信号量的值 + 1,代表这个线程执行完任务了,此时上面就可以唤醒一个休眠的线程进来执行代码了
dispatch_semaphore_signal(self.semaphore);
}
@end
// 控制台打印:
剩余票数:9,{number = 5, name = (null)}
剩余票数:8,{number = 4, name = (null)}
剩余票数:7,{number = 3, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 7, name = (null)}
剩余票数:4,{number = 8, name = (null)}
剩余票数:3,{number = 9, name = (null)}
剩余票数:2,{number = 10, name = (null)}
剩余票数:1,{number = 11, name = (null)}
剩余票数:0,{number = 12, name = (null)}
2.2 把多个线程放入GCD串行队列,让它们一个一个串行执行
- 虽然有多个线程,但是我们把多个线程放入串行队列,让它们串行执行,不就实现线程同步了嘛。
注意不要把任务同步和线程同步搞混了:
- 直接把所有任务放进串行队列,GCD就只会开辟一个线程来执行这些任务,只有一个线程,根本就不存在什么线程同步不同步,这是任务同步而不是线程同步。
- 还是把所有任务放进并发队列,GCD会开辟多个线程来执行这些任务,这才存在线程同步,我们再把这多个线程放进串行队列里让线程串行执行,这才是线程同步。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalTicketCount;
@property (nonatomic, strong) dispatch_queue_t queue; // 队列
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建串行队列
self.queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
self.totalTicketCount = 10;
// 这才是线程同步
for (int i = 0; i < 10; i++) {
// 这里是for循环,所以10个线程的开辟是有先后顺序的
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 把每个子线程的任务都放到串行队列里,这样这些子线程就是挨个执行的了
// 注意:这里得用dispatch_sync添加,用dispatch_async添加的话那就是在子线程里又开辟子线程了,又因为是串行队列,所以就只会开辟一个子线程,那10个任务就会在这一个子线程上串行执行,这就相当等于是任务同步了
// 假设线程1先开辟好,执行这个任务,就会卡住线程1,往self.queue添加售票任务、执行售票任务
// 如果线程1在卡住或者往self.queue添加售票任务、执行售票任务时,线程2开辟好进来了,就会卡住线程2,往self.queue添加售票任务、执行售票任务,但是必须得等线程1的售票任务完成才能执行,因为人家是先添进串行队列里的
dispatch_sync(self.queue, ^{
[self saleTicket];
});
});
}
}
- (void)saleTicket {
self.totalTicketCount--;
NSLog(@"剩余票数:%ld,%@", self.totalTicketCount, [NSThread currentThread]);
}
@end
// 控制台打印:
剩余票数:9,{number = 3, name = (null)}
剩余票数:8,{number = 4, name = (null)}
剩余票数:7,{number = 5, name = (null)}
剩余票数:6,{number = 6, name = (null)}
剩余票数:5,{number = 7, name = (null)}
剩余票数:4,{number = 8, name = (null)}
剩余票数:3,{number = 9, name = (null)}
剩余票数:2,{number = 10, name = (null)}
剩余票数:1,{number = 11, name = (null)}
剩余票数:0,{number = 12, name = (null)}
3、线程同步方案性能对比
性能从高到低依次为:os_unfair_lock
> 自旋锁 > GCD信号量(推荐使用) > 互斥锁(推荐使用) > GCD串行队列 > 递归锁。
os_unfair_lock
,性能最高,不过要iOS10之后才能用
自旋锁
OSSpinLock
,性能也很高,但是苹果已经不推荐使用了,用os_unfair_lock
- 设置线程的最大并发数为1——GCD信号量(推荐使用)
互斥锁
pthread_mutex
普通锁(推荐使用)NSLock
- 把多个线程放入串行队列——GCD串行队列
递归锁
pthread_mutex
递归锁NSRecursiveLock
@synchronized