说起“锁”,一般都要涉及多线程,因为一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。比如:多个线程访问同一个对象、同一个变量、同一个文件。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。这篇文章中,我将会整理一下 iOS 开发中几种多线程方案。
首先,我们来了解一下,iOS中常见的多线程方案:
一、iOS中常见的多线程方案有四种:
pthread : 纯粹 C 语言的API,跨平台, 线程生命周期程序员管理,几乎不用。
NSThread: OC 面向对象 API ,简单易用,可以直接操作线程对象, 线程生命周期程序员管理,偶尔使用。
GCD:旨在替代NSThread,充分利用 设备多核,C 语言的API,线程生命周期自动管理 ,经常使用。
NSOPeration:基于 GCD (底层就是GCD),比GCD多了些实用功能,OC 方法,自动管理线程,经常实用。
二、GCD简单介绍:
GCD中核心有四个概念:同步、异步、串行、并发
同步执行函数:dispatch_sync();
异步执行函数:dispatch_async();
串行队列: dispatch_queue_t queue = dispatch_get_main_queue();(主队列 是一种特殊的串行队列)
并发队列: ①全局的并发队列: dispatch_quque_t queue = dispatch_get_global_queue(0,0);
② 手动创建并发队列: dispatch_queue_t queue = dispatch_queue_create("myQueue",DISPATCH_QUEUE_CONCURRENT);
这里,我就不过多的阐述什么是同步、什么是异步、什么是串行、什么是并行等问题,需要了解的可自行查阅。下面我们来介绍本文的核心
三、多线程-安全隐患示例
开篇说过,当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。这里有一个经典的例子,火车票卖票和存取取钱的例子,我们分别展开来说一下:
例子一:卖火车票
卖票代码如下:
#import "ViewController.h"
@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self ticketTestAction];
}
/// 卖票演示
- (void)ticketTestAction {
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleSingleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleSingleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleSingleTicket];
}
});
}
/// 卖票事件
- (void)saleSingleTicket {
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
}
@end
打印结果如下:
2019-12-29 15:24:07.794256+0800 GCDDemo[4974:135412] 还剩12张票 - <NSThread: 0x600002a13040>{number = 5, name = (null)}
2019-12-29 15:24:07.794260+0800 GCDDemo[4974:135410] 还剩14张票 - <NSThread: 0x600002a6cc00>{number = 6, name = (null)}
2019-12-29 15:24:07.794317+0800 GCDDemo[4974:135416] 还剩13张票 - <NSThread: 0x600002a68880>{number = 7, name = (null)}
2019-12-29 15:24:07.794442+0800 GCDDemo[4974:135412] 还剩11张票 - <NSThread: 0x600002a13040>{number = 5, name = (null)}
2019-12-29 15:24:07.794471+0800 GCDDemo[4974:135410] 还剩10张票 - <NSThread: 0x600002a6cc00>{number = 6, name = (null)}
2019-12-29 15:24:07.794620+0800 GCDDemo[4974:135416] 还剩9张票 - <NSThread: 0x600002a68880>{number = 7, name = (null)}
2019-12-29 15:24:07.794703+0800 GCDDemo[4974:135410] 还剩8张票 - <NSThread: 0x600002a6cc00>{number = 6, name = (null)}
2019-12-29 15:24:07.794845+0800 GCDDemo[4974:135412] 还剩7张票 - <NSThread: 0x600002a13040>{number = 5, name = (null)}
2019-12-29 15:24:07.794889+0800 GCDDemo[4974:135410] 还剩6张票 - <NSThread: 0x600002a6cc00>{number = 6, name = (null)}
2019-12-29 15:24:07.794981+0800 GCDDemo[4974:135416] 还剩5张票 - <NSThread: 0x600002a68880>{number = 7, name = (null)}
2019-12-29 15:24:07.795366+0800 GCDDemo[4974:135412] 还剩4张票 - <NSThread: 0x600002a13040>{number = 5, name = (null)}
2019-12-29 15:24:07.798222+0800 GCDDemo[4974:135416] 还剩3张票 - <NSThread: 0x600002a68880>{number = 7, name = (null)}
2019-12-29 15:24:07.798612+0800 GCDDemo[4974:135412] 还剩2张票 - <NSThread: 0x600002a13040>{number = 5, name = (null)}
2019-12-29 15:24:07.799321+0800 GCDDemo[4974:135416] 还剩1张票 - <NSThread: 0x600002a68880>{number = 7, name = (null)}
2019-12-29 15:24:07.800157+0800 GCDDemo[4974:135410] 还剩0张票 - <NSThread: 0x600002a6cc00>{number = 6, name = (null)}
眨一看,貌似没什么问题,但是细看下,发现剩余票数的计算确实存在问题,刚上来还只剩余了12张,但是又卖了一张,就剩余14张了,出现了越卖越多的情况,这显然是不合理的,这就是多线程出现的问题。
例子二 、存钱取钱
存钱取钱代码如下:
#import "ViewController.h"
@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@property (assign, nonatomic) int moneyCount;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self moneyTest];
}
/// 存钱、取钱演示
- (void)moneyTest {
self.moneyCount = 50;
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.moneyCount;
sleep(.2);
oldMoney += 100;
self.moneyCount = oldMoney;
NSLog(@"存100,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
/// 取钱
- (void)drawMoney {
int oldMoney = self.moneyCount;
sleep(.2);
oldMoney -= 50;
self.moneyCount = oldMoney;
NSLog(@"取50,还剩%d元 - %@", oldMoney, [NSThread currentThread]);
}
@end
打印结果如下:
2019-12-29 15:32:59.075956+0800 GCDDemo[5180:155847] 取50,还剩0元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.075962+0800 GCDDemo[5180:155845] 存100,还剩100元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.076154+0800 GCDDemo[5180:155847] 取50,还剩50元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.076147+0800 GCDDemo[5180:155845] 存100,还剩200元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.076331+0800 GCDDemo[5180:155847] 取50,还剩150元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.076496+0800 GCDDemo[5180:155847] 取50,还剩100元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.076628+0800 GCDDemo[5180:155847] 取50,还剩150元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.076574+0800 GCDDemo[5180:155845] 存100,还剩200元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.076817+0800 GCDDemo[5180:155847] 取50,还剩100元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.077335+0800 GCDDemo[5180:155845] 存100,还剩200元 - <NSThread: 0x6000035df400>{number = 2019-12-29 15:32:59.077901+0800 GCDDemo[5180:155847] 取50,还剩150元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
4, name = (null)}
2019-12-29 15:32:59.078920+0800 GCDDemo[5180:155847] 取50,还剩100元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.079308+0800 GCDDemo[5180:155845] 存100,还剩200元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.079491+0800 GCDDemo[5180:155847] 取50,还剩150元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.079942+0800 GCDDemo[5180:155845] 存100,还剩250元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.080078+0800 GCDDemo[5180:155847] 取50,还剩200元 - <NSThread: 0x6000035b2e40>{number = 5, name = (null)}
2019-12-29 15:32:59.080475+0800 GCDDemo[5180:155845] 存100,还剩300元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.080968+0800 GCDDemo[5180:155845] 存100,还剩400元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.081394+0800 GCDDemo[5180:155845] 存100,还剩500元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
2019-12-29 15:32:59.097714+0800 GCDDemo[5180:155845] 存100,还剩600元 - <NSThread: 0x6000035df400>{number = 4, name = (null)}
一开始我们设定银行卡余额为50元,即:self.moneyCount = 50;
,假设每次存100元,取50元,操作10次以后,银行卡余额应该剩余300元。但是打印结果却是600元。(哈哈哈,如果现实中真的有这么好的事儿多开心。😁)
经过分析我们也可以发现,这也是因为多线程造成的问题。如何解决这个问题呢?通过这两个经典的例子,我们引出了线程锁的方案。
四、线程同步方案(线程锁)
线程同步最常见的方法就是线程加锁,以下是iOS中常见的线程锁:
◼ OSSpinLock(自旋锁)
◼ os_unfair_lock
◼ pthread_mutex
◼ dispatch_semaphore
◼ dispatch_queue(DISPATCH_QUEUE_SERIAL)
◼ NSLock
◼ NSRecursiveLock
◼ NSCondition
◼ NSConditionLock
◼ @synchronized
我们先来依次看下这几种锁的基本用法:
(1).◼ OSSpinLock(自旋锁)
卖票加锁后的代码如下:
#import "ViewController.h"
#import <libkern/OSAtomic.h>
@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@property (assign, nonatomic) OSSpinLock ticketLock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.ticketLock = OS_SPINLOCK_INIT;
[self ticketTestAction];
}
/// 卖票演示
- (void)ticketTestAction {
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleSingleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleSingleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0; i < 5; i++) {
[self saleSingleTicket];
}
});
}
/// 卖票事件
- (void)saleSingleTicket {
// 加锁
OSSpinLockLock(&_ticketLock);
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_ticketLock);
}
@end
此时,加锁后,我们再次运行查看结果,发现票的剩余张数都是正常的了,说明OSSpinLock这个锁解决了刚刚多线程的问题。
总结OSSpinLock锁的核心代码如下
需要导入头文件 #import <libkern/OSAtomic.h>
// 初始化锁
OSSpinLock *lock = OS_SPINLOCK_INIT;
// 尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
bool result = OSSpinLockTry(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);
(2).◼ os_unfair_lock
需要导入头文件 #import <os/lock.h>
os_unfair_lock核心代码如下:
// 初始化
os_unfair_lock *lock = &OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(lock);
// 加锁
os_unfair_lock_lock(lock);
// 解锁
os_unfair_lock_unlock(lock);
(3).◼ pthread_mutex
需要导入头文件 #import <pthread.h>
pthread_mutex -默认锁 核心代码如下:
// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t mutex ;
pthread_mutex_init(&mutex, &attr);
// 尝试加锁
pthread_mutex_trylock(&mutex);
// 加锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁相关资料
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);
注意:这里pthread_mutexattr_settype 里面有好几种类型的锁
#define PTHREAD_MUTEX_NORMAL 0
#define PTHREAD_MUTEX_ERRORCHECK 1
#define PTHREAD_MUTEX_RECURSIVE 2 -- 递归锁
#define PTHREAD_MUTEX_DEFAULT
PTHREAD_MUTEX_NORMAL
pthread_mutex -条件锁 核心代码如下:
// 初始化锁
pthread_mutex_t mutex;
// NULL代表使用默认属性
pthread_mutex_init(&mutex, NULL);
// 初始化条件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
// 等待条件(进入休眠解锁;唤醒后加锁)
pthread_cond_wait(&condition, &mutex);
// 激活一个等待该条件的线程
pthread_cond_signal(&condition);
// 激活所有等待该条件的线程
pthread_cond_broadcast(&condition);
// 销毁资源
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);
(4).◼NSLock
NSLock是对mutex普通锁的封装
NSLock核心代码如下:
// 初始化
NSLock *lock = [[NSLock alloc] init];
// 加锁
[lock lock];
// 解锁
[lock unlock];
(5).◼ NSRecursiveLock
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSRecursiveLock核心代码如下:
// 初始化
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
// 加锁
[lock lock];
// 解锁
[lock unlock];
(6).◼NSCondition
NSCondition是对mutex和cond的封装
NSCondition核心代码如下:
// 初始化
NSCondition *condition = [[NSCondition alloc] init];
// 加锁
[condition lock];
// 解锁
[condition unlock];
(7).◼ NSConditionLock
NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
NSConditionLock核心代码如下:
// 初始化
NSConditionLock *condition = [[NSConditionLock alloc]initWithCondition:0];
// 获得锁
[condition lockWhenCondition:1];
// 解锁
[condition unlockWithCondition:1];
(8).◼ dispatch_semaphore
dispatch_semaphore核心代码如下:
// 信号量初始值
int value = 1;
// 初始化信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
// 如果信号量的值<= 0 当前线程进入休眠等待
// 如果信号量的值> 0 信号量值减1,执行下面的代码
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值加1
dispatch_semaphore_signal(semaphore);
(9).◼dispatch_queue(DISPATCH_QUEUE_SERIAL)
核心代码如下:
dispatch_queue_t queue = dispatch_queue_create(@"local_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//执行任务
});
(10).◼ @synchronized
synchronized核心代码如下:
@synchronized是对mutex递归锁的封装
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
// obj加锁的对象,可以是self 或者具体的static对象
@synchronized (obj) {
//执行任务
}
以上几种锁的用法简单介绍完了。它们的性能从高到低,排序依次是:
性能从高到低排序
◼ 1. os_unfair_lock
◼ 2. OSSpinLock
◼ 3. dispatch_semaphore
◼ 4. pthread_mutex
◼ 5. dispatch_queue(DISPATCH_QUEUE_SERIAL)
◼ 6. NSLock
◼ 7. NSCondition
◼ 8. pthread_mutex(recursive)
◼ 9. NSRecursiveLock
◼ 10. NSConditionLock
◼ 11. @synchronized
@synchronized加锁方案是最不推荐的一种,因为新能最差。
iOS中常见的几种锁及其用法就简单介绍到这里,方便自己日后查阅学习。
网友评论