06、多线程
GCD
GCD中的基本概念
-
同步执行(sync)
(1)同步执行只在当前线程中执行任务,不具备开启新线程的能力。
(2)同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后才会继续执行任务。 -
异步执行(async)
(1)异步执行可以在新的线程中执行任务,具备开启新线程的能力。
(2)异步添加任务到指定的队列中,它不会做任何等待,继续执行其他任务。 -
注意:
异步执行虽然具备开启新线程的能力,但是并不一定能开启,这跟任务所指定的队列类型有关。 -
串行队列
只开启一个线程,任务是一个接着一个的执行。 -
并发队列
可以开启多个线程,并且同时执行任务。并发队列的并发功能只在异步函数下才有效。 -
注意:
同步执行和异步执行决定了是否开启线程,串行队列和并发队列决定是开启一个子线程还是多个子线程。 -
主队列
主队列是GCD提供的一种特殊的串行队列,所有放在主队列中的任务,都会放到主线程中执行。
放在主队列中的任务会在主线程空闲的时候被执行。 -
全局并发队列
全局并发队列是GCD提供的一个默认的并发队列。
同步/异步和串行/并行
//同步分派一个任务到串行队列上面
dispatch_aync(serial_queue,^{//任务})
//异步分派一个任务到串行队列上面
dispatch_async(serial_queue,^{//任务})
//同步分派一个任务到并发任务队列上面
dispatch_sync(concurrent_queue,^{//任务})
//异步分派一个任务到并发队列上面
dispatch_async(concurrent_queue,^{//任务})
同步+主队列
同步执行+主队列在不同线程中调用结果也是不一样的,在主线程中调用会出现死锁,而在其他线程中则不会。
- (void)viewDidLoad {
dispatch_sync(dispatch_get_main_queue(), ^{
sleep(1);
NSLog(@"执行任务1");
});
NSLog(@"执行任务2");
}
同步执行不具备开启新线程的能力,并且执行任务的时候需要等待任务执行完成之后才会继续向下执行,会堵塞当前线程。而当前线程又是主线程,也就是阻塞了主线程。主队列中的任务时提交到主线程去执行的,需要等到主线程空闲的时候才会执行,这样相互等待,最终造成死锁。
同步+串行队列
-(void) viewDidLoad{
//创建串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.lichangan.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
sleep(1);
NSLog(@"执行任务1:%@",NSThread.currentThread);
});
NSLog(@"执行任务2:%@",NSThread.currentThread);
}
同步执行不具备开启新线程的能力,并且执行任务的时候需要等待任务执行完成之后才会继续向下执行,会堵塞当前线程。所以不会开启新线程,提交的任务依然在主线程中执行。
执行任务1:<NSThread: 0x6000024c43c0>{number = 1, name = main}
执行任务2:<NSThread: 0x6000024c43c0>{number = 1, name = main}
同步+并发队列
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"执行任务1:%@",NSThread.currentThread);
//创建并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.lichangan.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
dispatch_sync(concurrentQueue,^{
NSLog(@"执行任务2:%@",NSThread.currentThread);
dispatch_sync(globalQueue,^{
NSLog(@"执行任务3:%@",NSThread.currentThread);
});
NSLog(@"执行任务4:%@",NSThread.currentThread);
});
NSLog(@"执行任务5:%@",NSThread.currentThread);
}
同步执行不具备开启新线程的能力,并且执行任务的时候需要等待任务执行完成之后才会继续向下执行,会堵塞当前线程。所以不会开启新线程,提交的任务依然在主线程中执行。
执行任务1:<NSThread: 0x6000004601c0>{number = 1, name = main}
执行任务2:<NSThread: 0x6000004601c0>{number = 1, name = main}
执行任务3:<NSThread: 0x6000004601c0>{number = 1, name = main}
执行任务4:<NSThread: 0x6000004601c0>{number = 1, name = main}
执行任务5:<NSThread: 0x6000004601c0>{number = 1, name = main}
异步+主队列
-(void)viewDidLoad{
dispatch_async(dispatch_get_main_queue,^{
sleep(1);
NSLog(@"执行任务1");
});
NSLog(@"执行任务2");
}
异步执行可以在新的线程中执行任务,具备开启新线程的能力,不会堵塞当前线程.但是主队列中的任务会提交到主线程中,等到主线程空闲的时候执行,所以任务还是在主线程中执行。
执行任务2:<NSThread: 0x600003edc3c0>{number = 1, name = main}
执行任务1:<NSThread: 0x600003edc3c0>{number = 1, name = main}
异步+串行队列
-(void)viewDidLoad{
dispatch_queue_t serialQueue = dispatch_queue_create("com.lichangan.queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue,^{
sleep(1);
NSLog(@"执行任务1:%@",NSThread.currentThread);
});
NSLog(@"执行任务2:%@",NSThread.currentThread);
}
异步执行可以在新的线程中执行任务,具备开启新线程的能力,不会堵塞当前线程。因为是串行队列,只能开启一个子线程,所以任务是在子线程中串行执行的。
执行任务2:<NSThread: 0x6000037ac100>{number = 1, name = main}
执行任务1:<NSThread: 0x6000037e8fc0>{number = 6, name = (null)}
异步+并发队列
-(void)viewDidLoad{
dispatch_async(dispatch_get_global_queue(0, 0),^{
NSLog(@"执行任务1:%@",NSThread.currentThread);
NSLog(@"执行任务2:%@",NSThread.currentThread);
});
NSLog(@"执行任务3:%@",NSThread.currentThread);
}
异步执行可以在新的线程中执行任务,具备开启新线程的能力,不会堵塞当前线程。因为是并发队列,会开启多个子线程并发执行
因为是并发执行可能会出现多种执行结果:
执行任务1:<NSThread: 0x600000259f40>{number = 5, name = (null)}
执行任务3:<NSThread: 0x6000002501c0>{number = 1, name = main}
执行任务2:<NSThread: 0x600000259f40>{number = 5, name = (null)}
或者
执行任务1:<NSThread: 0x600000259f40>{number = 5, name = (null)}
执行任务2:<NSThread: 0x600000259f40>{number = 5, name = (null)}
执行任务3:<NSThread: 0x6000002501c0>{number = 1, name = main}
performSelector:withObject:afterDelay:不调用
-(void)viewDidLoad{
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(dispatch_get_global_queue(0, 0),^{
NSLog(@"执行任务1:%@",NSThread.currentThread);
[self performSelector:@selector(printLog) withObject:nil afterDelay:0];
NSLog(@"执行任务3:%@",NSThread.currentThread);
});
}
- (void) printLog{
NSLog(@"执行任务2:%@",NSThread.currentThread);
}
执行任务1:<NSThread: 0x600001217040>{number = 6, name = (null)}
执行任务3:<NSThread: 0x600001217040>{number = 6, name = (null)}
我们以异步的方式将任务添加到全局并发队列中,这个block会在GCD底层维护的线程池上面的某个一个线程上去执行,对于底层的线程池中的线程,默认是没有开启RunLoop的。而performSelector:withObject:afterDelay:会在子线程中开启一个NSTimer定时器执行selector里面的方法,即使延迟0s执行任务。子线程没有创建和开启runLoop导致定时器无法工作。
解决办法:
(1)在子线程中手动开启当前线程的runLoop并运行,但是你还必须要保证你的延迟时间到时runLoop还在运行着,修改后即可解决
[self performSelector:@selector(printLog) withObject:nil afterDelay:0];
// 为了防止runLoop返回,给runLoop添加一些空的源,让runLoop一直运行
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopSourceContext sourceCtx = {
.version = 0,
.info = NULL,
.retain = NULL,
.release = NULL,
.copyDescription = NULL,
.equal = NULL,
.hash = NULL,
.schedule = NULL,
.cancel = NULL,
.perform = NULL
};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(NULL, 0, &sourceCtx);
CFRunLoopAddSource(runLoop, source, kCFRunLoopDefaultMode);
// 子线程的runLoop需要调用run才会运行
// 当前线程等待,但让出当前线程时间片,然后过afterDelay秒后返回
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:afterDelay]];
(2)直接使用dispatch_after,它里面的定时器不受runLoop影响的
(3)在主线程执行performSelector:withObject:afterDelay:
if ([[NSThread currentThread] isMainThread]) {
// 当前在子线程中,有afterDelay参数的方法不会被执行
[self performSelector:@selector(delay) withObject:nil afterDelay:0.3];
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
[self performSelector:@selector(delay) withObject:nil afterDelay:afterDelay];
});
}
GCD全局队列有哪几种
首先全局队列肯定是并发队列。如果不指定优先级,就是默认(default)优先级。另外还有backgroud、utility、user-initiated、unspecified、user-interactive。下面按照优先级顺序从低到高来排列:
- (1)Background:用来处理特别耗时的后台操作,例如同步、备份数据。
- (2)Utility:用来处理需要一点时间而又不需要立刻返回结果的操作、特别适合用于异步操作,例如下载、导入数据。
- (3)Default:默认优先级。一般来说开发者应该指定优先级。属于特殊情况。
- (4)User-initiated:用来处理用户触发的、需要立即返回结果的操作。比如打开用户点击的文件。
- (5)User-Interactive:用来处理用户交互的操作。一般用于主线程,如果不及时响应就可能阻塞主线程的操作。
- (6)Unspecified:未确定优先级,由系统根据不同环境推断。比如使用过时的API不支持优先级,此时可以设定为未确定的优先级,属于特殊情况。
dispatch_barrier_async
作用:与并发队列结合,可以高效率的避免数据竞争的问题
在队列中,barrier块必须单独执行,不能与其他block块并行执行。dispatch_barrier_async的block运行时机是在它之前所有的任务执行完毕,并且在它后面的任务开始执行之前,期间不会有其他的任务执行。注意在barrier执行的时候,队列本质上如同一个串行队列,其执行完以后才会恢复到并行队列。
怎样利用GCD实现多读单写呢?
假设我们在内存中维护一个字典或者DB文件,有多个读者和写者都要操作这个共享数据。
要解决的问题:
- (1)读者之间可以并发访问
- (2)读者和写者应该互斥
- (3)写者和写者互斥,同一时间有多线程执行写操作会导致程序异常或者数据错乱。
class UserCenter {
var userCenterDict:[String:Any]// 创建数据容器
let concurrent_queue:DispatchQueue
init(){
//创建一个并发队列
concurrent_queue = DispatchQueue(label: "com.cnlod.patient", qos: DispatchQoS.utility, attributes: DispatchQueue.Attributes.concurrent)
userCenterDict = []
}
func object(key:String)->Any?{
var obj:Any?
//同步执行任务,保证我们可以立即得到读取的内容
concurrent_queue.sync {
obj = userCenterDict[key]
}
return obj
}
func setObject(value:Any,key:String){
//异步栅栏设置数据
concurrent_queue.async(flags: DispatchWorkItemFlags.barrier) { [weak self] in
self?.userCenterDict[key] = value
}
}
}
#import "UserCenter.h"
@interface UserCenter()
{
// 定义一个并发队列
dispatch_queue_t concurrent_queue;
// 用户数据中心, 可能多个线程需要数据访问
NSMutableDictionary *userCenterDic;
}
@end
// 多读单写模型
@implementation UserCenter
- (id)init
{
self = [super init];
if (self) {
// 通过宏定义 DISPATCH_QUEUE_CONCURRENT 创建一个并发队列
concurrent_queue = dispatch_queue_create("read_write_queue", DISPATCH_QUEUE_CONCURRENT);
// 创建数据容器
userCenterDic = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(NSString *)key
{
__block id obj;
//同步执行任务,保证我们可以立即得到读取的内容
dispatch_sync(concurrent_queue, ^{
obj = [userCenterDic objectForKey:key];
});
return obj;
}
- (void)setObject:(id)obj forKey:(NSString *)key
{
// 异步栅栏调用设置数据
dispatch_barrier_async(concurrent_queue, ^{
[userCenterDic setObject:obj forKey:key];
});
}
@end
上面的代码中,setter方法使用了barrier block以后,对象的读操作依然是可以并发执行的,但是写入操作就必须单独执行了。
- (1)首先,我们需要创建一个私有的并行队列来处理读写操作,在这里不应该使用global_queue,因为我们通过dispatch_barrier_async来保证写操作的互斥,我们不希望写操作阻塞global_queue中的其他不相关的任务,我们只希望在写的同时,不会有其他的写操作或者读操作。
- (2)同时,也不推荐给队列设置优先级,多数情况下使用默认default就可以了。而改变优先级往往会造成一些无法预料的问题,比如优先级反转。
- (3)dispatch_barrier_async的block运行时机是在它之前所有的任务执行完毕,并且在它后面的任务开始执行之前,期间不会有其他的任务执行。注意在barrier执行的时候,队列本质上如同一个串行队列,其执行完以后才会恢复到并行队列。
- (4)另外一个值得注意的问题是:在写操作的时候,我们使用dispatch_barrier_async,而在读操作的时候我们使用dispatch_sync。这两个操作一个是异步的,一个是同步的。我们不需要使每次程序执行的时候都等待写操作完成,但是我们需要同步的执行读操作来保证程序能够立刻得到它想要的值。
- (5)使用sync的时候需要极其的小心,因为稍不注意,就有可能产生死锁,这可能造成灾难性的后果。
dispatch_group
有时候我们会有这样的需求:分别异步执行两个耗时任务,然后当两个耗时任务都执行完毕之后再回到主线程执行任务。这个时候就需要用到dispatch_group。
- (1)dispatch_group_create创建一个调度任务组
dispatch_group_t
dispatch_group_create(void);
- (2)dispatch_group_async 把一个任务异步提交到任务组里
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
group 提交到的任务组,这个任务组的对象会一直持续到任务组执行完毕
queue 提交到的队列,任务组里不同任务的队列可以不同
block 提交的任务
- (3)dispatch_group_enter/dispatch_group_leave
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
dispatch_group_enter
和 dispatch_group_leave
一般是成对出现的, 进入一次,就得离开一次。也就是说,当离开和进入的次数相同时,就代表任务组完成了。如果enter比leave多,那就是没完成,如果leave调用的次数错了, 会崩溃的;
这种方式用在不使用dispatch_group_async
来提交任务的情况下使用的。
- (4)dispatch_group_notify 用来监听任务组事件的执行完毕
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
group 监听的任务组
queue 执行完毕的这个闭包所在的队列
block 执行完毕所响应的任务
- (5)dispatch_group_wait 设置等待时间,在等待时间结束后,如果还没有执行完任务组,则返回。返回0代表执行成功,非0则执行失败
long dispatch_group_wait(dispatch_group_t group,
dispatch_time_t timeout);
使用GCD实现这个需求:A、B、C三个任务并发,完成后执行任务D?
class Group {
let concurrent_queue:DispatchQueue
var urls:[URL]
init(){
concurrent_queue = DispatchQueue(label: "com.cnlod.patient", qos: DispatchQoS.utility, attributes: DispatchQueue.Attributes.concurrent)
urls = []
}
func handle() {
//创建一个group
let group = DispatchGroup()
for url in urls {
// 异步组分派到并发队列当中
concurrent_queue.async(group: group) {
//根据url去下载图片
print("url is \(url)")
}
}
group.notify(queue: DispatchQueue.main) {
// 当添加到组中的所有任务执行完成之后会调用该Block
print("所有的图片已经下载完成")
}
}
func handle() {
let group = DispatchGroup()
for url in urls {
group.enter()
DispatchQueue.global().async {
//下载图片
group.leave()
}
}
}
}
#import "GroupObject.h"
@interface GroupObject()
{
dispatch_queue_t concurrent_queue;
NSMutableArray <NSURL *> *arrayURLs;
}
@end
@implementation GroupObject
- (id)init
{
self = [super init];
if (self) {
// 创建并发队列
concurrent_queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
arrayURLs = [NSMutableArray array];
}
return self;
}
- (void)handle
{
// 创建一个group
dispatch_group_t group = dispatch_group_create();
// for循环遍历各个元素执行操作
for (NSURL *url in arrayURLs) {
// 异步组分派到并发队列当中
dispatch_group_async(group, concurrent_queue, ^{
//根据url去下载图片
NSLog(@"url is %@", url);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 当添加到组中的所有任务执行完成之后会调用该Block
NSLog(@"所有图片已全部下载完成");
});
}
@end
信号量 Semaphore_signal
基本用法
- (1)创建一个信号量
swift:
DispatchSemaphore(value: Int)
objctive-c:
dispatch_semaphore_t dispatch_semaphore_create(long value);
创建一个信号量,参数是并发的初始值。
函数内部实现创建了一个结构体,里面有两个成员变量,一个是信号量的值int value
,另外一个是标识线程的一个线程列表。
struct semaphore {
int value;
List<thread>;
}
- (2)等待信号
swift:
public func wait()
public func wait(timeout: DispatchTime) -> DispatchTimeoutResult
public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult
objective-c:
long dispatch_semaphore_wait(dispatch_semaphore_t dsema,
dispatch_time_t timeout);
等待信号,表示前面的队列已经满了,现在只能等待。
该函数会使信号总量减1,然后获取信号总量,如果信号总量大于0,则可以正常执行后面的任务,如果信号总量小于等于0会一直等待,阻塞当前线程。
内部实现:当我们调用wait操作的时候,它会先对value值进行减1操作,然后获取信号量,如果信号量小于0,说明没有资源可以访问,或者无法获取到信号量,获取信号量的线程会通过一个主动的行为将自己阻塞起来。
{
S.value = S.value - 1
if S.value < 0 then Block(S.List); ==>阻塞是一个主动行为
}
- (3)发送信号
swift:
public func signal() -> Int
objective-c:
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);
发送一个信号,表示我即将离开这个队列,通知等待着的你可以准备进入。
内部实现:首先会对信号总量执行加1操作,然后获取信号总量,如果信号总量小于等于0说明有队列在排队。在之前执行wait操作时,如果没有获取到信号量,将会把自己主动阻塞到S.List当中,也就意味着有对应的线程需要唤醒,由释放信号的线程来唤醒一个阻塞的线程。
{
S.value = S.value + 1
if S.value <= 0 then wakeup(S.List) ==>唤醒时一个被动行为
}
- 注意: Semaphore object deallocated while in use
信号量在被销毁时,信号量的值必须大于或者等于初始化的值。
GCD最大并发数的控制
override func viewDidLoad() {
super.viewDidLoad()
//创建调度组
let group = DispatchGroup()
//创建信号量,初始值为6,也就是意味着最大并发数为6,最多允许6个任务同时执行,其他任务只能等待。
let semaphore = DispatchSemaphore(value: 6)
//创建一个并发队列
let concurrentQueue = DispatchQueue(label: "com.lichangan.concurrent", qos: DispatchQoS.default, attributes: DispatchQueue.Attributes.concurrent)
//循环创建多个线程
for _ in 0 ... 50 {
//每次执行一次wait操作,信号量的值都会减去1
//执行6次之后,信号量的值为0,这个时候会阻塞当前线程。
//所以最大并发量为6
semaphore.wait()
concurrentQueue.async(group: group) {
sleep(3)
//耗时操作完成,通知下一个等待的任务进入
let result = semaphore.signal()
print(result)
}
}
//保证线程组里面的所有线程执行完后才能进行下一步
group.notify(queue: DispatchQueue.main) {
print("耗时任务全部处理完毕,主线程更新UI")
}
}
刚开始同步创建6个并发操作,达到最大值时会阻塞for循环,开始等待某个semphore完成耗时操作,for循环会进行一次。
这样就实现了通过信号量实现GCD的最大并发数的控制。
使并发任务完成同步操作
override func viewDidLoad() {
super.viewDidLoad()
//创建调度组
let group = DispatchGroup()
//创建信号量,最大并发数为1,同时只能执行一个任务
let semaphore = DispatchSemaphore(value: 1)
let queue = DispatchQueue(label: "concurrentQueue", attributes: DispatchQueue.Attributes.concurrent)
group.enter()
queue.async {
semaphore.wait() //等待信号量,阻塞当前线程
print("执行任务1")
group.leave()
semaphore.signal()//任务执行完毕,发送信号量
}
group.enter()
queue.async {
semaphore.wait() //等待信号量,阻塞当前线程
for _ in 0 ... 10000 {
print("waiting") //耗时操作
}
print("执行任务2")
group.leave()
semaphore.signal() //等待信号量,阻塞当前线程
}
group.enter()
queue.async {
semaphore.wait() //等待信号量,阻塞当前线程
print("执行任务3")
group.leave()
semaphore.signal() //信号量在销毁时,信号量的值必须大于或者等于初始值,防止崩溃
}
group.notify(queue: DispatchQueue.main) {
print("更新UI")
}
}
打印结果是:执行任务1、执行任务2、执行任务3、更新UI
虽然它们都在不同的子线程中,但是确实以同步的方式执行任务,充分发挥了CPU多线程的优势。虽然使用异步函数+串行队列实现任务同步更加简单,但是由于只开启了一个子线程,丧失了并发执行的可能性。对于一些任务3的执行依赖于任务2、任务2的执行依赖于任务1,在GCD中可以通过这种方式解决这个需求。
加锁解决数据竞争问题
信号量也是一种互斥锁,它的效率是和OSSpinLock差不多的。
-(void) viewDidLoad{
//1.创建全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//2.创建dispatch_semaphore_t对象
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
//3.创建保存数据的可变数组
NSMutableArray * array = [NSMutableArray array];
//执行10000次操作
for (int i =0 ; i<1000; i++) {
//异步添加数据
//数据进入,等待处理,信号量减1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_async(queue, ^{
//处理数据
[array addObject:@(i)];
//数据处理完毕,信号量加1,等待下一次处理
dispatch_semaphore_signal(semaphore);
});
}
NSLog(@"%@",array);
}
实战,以下代码有什么隐患?
func getUser(id:String) throws->User {
return try storage.getUser(id)
}
func setUser(_ user:User) throws {
tyr storage.setUser(user)
}
上面这段代码的功能是读写用户信息。咋一看上去没有任何问题,但是一旦多线程涉及到读写,就会产生竞态条件(race condition)。解决办法就是打开Xcode中的线程检测工具thread sanitizer(在Xcode的scheme中勾选Thread Sanitizer即可)。它会检测出代码中出现竞态条件之处,并提醒我们修改。
对于读写问题,一般有三种处理方式。
- 第一种是用串行队列配合同步,无论读写,同一时间只能做一个操作,这样就保证了队列的安全。其缺点是速度慢,尤其是在大量读写发生时,每次只能做单个读或写操作的效率实在太低。修改代码如下:
func getUser(id:String) throws->User {
return serialQueue.sync {
return try storage.getUser(id)
}
}
func setUser(_ user User) throws {
try serialQueue.sync {
try storage.setUser(user)
}
}
- 第二种是用串行队列配合异步操作完成,异步操作会直接返回,所以必须配合逃逸闭包来保证后续操作的合法性。这种方法仍然存在同一时间只能处理一个操作的问题。其缺点是速度慢,尤其是在大量读写发生时,每次只能做单个读或写操作的效率实在太低。
enum Result<T> {
case value(T)
case error(Error)
}
func getUser(id:String,completion:@escaping(Result<User>)->Void){
serialQueue.async {
do {
user = try storage.getUser(id)
completion(.value(user))
}catch {
completion(.error(error))
}
}
}
func setUser(_ user:User,completion:@escaping(Result<()>)->Void) {
serialQueue.async {
do {
user = try storage.setUser(user)
completion(.value(()))
}catch {
completion(.error(error))
}
}
}
- 第三种方法是用并行队列,读取时使用sync直接返回,写操作时使用barrier flag 来保证此时并行队列只进行当前的写操作(类似将并行队列暂时转为串行队列),而无视其他操作。
func getUser(id:String) throws->User {
let user:User!
concurrentQueue.sync {
user = try storage.getUser(id)
}
return user
}
func setUser(_ user:User,completion:@escaping (Result<()>)->Void) throws{
concurrentQueue.async(flags: .barrier) {
do {
try storage.setUser(user)
completion(.value(()))
}catch {
completion(.error(error))
}
}
}
dispatch_apply
void dispatch_apply(size_t iterations, dispatch_queue_t queue,
DISPATCH_NOESCAPE void (^block)(size_t));
该函数按指定的次数将指定的Block追加到指定的Dispatch Queue中,并等待全部处理执行结束,好处是可以重复执行某项操作并复用我们的Block了
第一个参数为重复次数;
第二个参数为追加对象的Dispatch Queue;
第三个参数为追加的操作,追加的Block中带有参数,这是为了按第一个参数重复追加Block并区分各个Block而使用。
dispatch_apply类似一个for循环,会在指定的dispatch queue中运行block任务n次,可以实现一个高性能的循环迭代。如果队列是并发队列,则会并发执行block任务,dispatch_apply是一个同步调用,block任务执行n次后才返回。
高效循环实现原理:将循环的每次迭代提交到dispatch queue进行处理,结合并发queue使用时,可以并发地执行迭代以提高性能。但是也不是任何一个循环都需要用dispatch_apply来替换,因为dispatch queue还是存在一些开销的,虽然非常小。所以只有当你的循环代码拥有足够的工作量,才能忽略掉dispatch queue的这些开销以提高性能。 得出来得结果由于是多线程,所以是无序得但是每个索引对应的值还是一样的。
dispatch_apply 会阻塞当前线程,推荐在 dispatch_async 中执行。
dispatch_apply 函数结合concurrent queue,dispatch_apply能实现一个高性能的循环迭代。
dispatch_apply+并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(5, queue, ^(NSUInteger index) {
NSLog(@"thread = %@ , index = %ld",[NSThread currentThread],index);
});
NSLog(@"执行完毕");
执行结果:2、1、3、0、4、执行完毕。当传入的queue是并行队列时,重复执行的操作输出是无序的。
dispatch_apply+串行队列
dispatch_queue_t queue = dispatch_queue_create("com.serial", DISPATCH_QUEUE_SERIAL);
dispatch_apply(5, queue, ^(NSUInteger index) {
NSLog(@"thread = %@ , index = %ld",[NSThread currentThread],index);
});
NSLog(@"执行完毕");
执行结果:0、1、2、3、、4、执行完毕,串行执行任务。
暂停和重启队列
在Dispatch Queue执行任务时,如果我们想暂停队列,可以使用dispatch_suspend函数,重新让队列执行任务可以使用dispatch_resume。这里要注意的是暂停队列只是让队列暂时停止执行下一个任务,而不是中断当前正在执行的任务。
dispatch_queue_set_specific & dispatch_get_specific
dispatch_queue_set_specific
有时候我们需要将某些东西关联到队列上,比如我们想在某个队列上存一个东西,或者我们想区分2个队列。GCD提供了dispatch_queue_set_specific方法,通过key,将context关联到queue上
Swift:
func setSpecific<T>(key: DispatchSpecificKey<T>, value: T?)
Objective-C:
void dispatch_queue_set_specific(dispatch_queue_t queue, const void *key, void *context, dispatch_function_t destructor);
- queue:需要关联的queue,不允许传入NULL
- key:唯一的关键字
- context:要关联的内容,可以为NULL
- destructor:释放context的函数,当新的context被设置时,destructor会被调用
dispatch_get_specific
Swift:
func getSpecific<T>(key: DispatchSpecificKey<T>) -> T?
Objective-C:
void *dispatch_queue_get_specific(dispatch_queue_t queue, const void *key);
void *dispatch_get_specific(const void *key);
dispatch_queue_get_specific: 根据queue和key取出context,queue参数不能传入全局队列
dispatch_get_specific: 根据唯一的key取出当前queue的context。如果当前queue没有key对应的context,则去queue的target queue取,取不着返回NULL,如果对全局队列取,也会返回NULL
iOS 6之后dispatch_get_current_queue()被废弃。如果我们需要区分不同的queue,可以使用set_specific方法。根据对应的key是否有值来区分。
如何判断当前执行的队列是不是指定队列
关于这个问题方法有三:
(1)使用dispatch_get_current_queue获取当前执行的队列。
(2)使用dispatch_queue_set_specific & dispatch_get_specific 标记并获取指定队列
(3)使用dispatch_queue_get_label 获取队列标签,比较字符串判断。(SDWebImage中采用此法判断)
其中,dispatch_get_current_queue在iOS6之后是被弃用的,苹果只推荐在打印中使用,原因是它容易导致死锁。
Swift代码
let queueSpecificValue = "queueA"
let queueSpecificKey = DispatchSpecificKey<String>()
//创建串行队列
let queueA = DispatchQueue(label: "com.lichangan.queueA")
//标记这个队列
queueA.setSpecific(key: queueSpecificKey, value: queueSpecificValue)
queueA.async {
let block = {
let name = queueA.getSpecific(key: queueSpecificKey)
print("执行当前任务的队列是:" + (name ?? ""))
}
//获取队列标记
let name = queueA.getSpecific(key: queueSpecificKey)
if name != nil {
block()
}else{
queueA.async {
block()
}
}
}
打印结果: 执行当前任务的队列是:queueA
OC代码:
//创建一个串行队列
dispatch_queue_t queueA = dispatch_queue_create("com.lichangan.queueA", NULL);
static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void *)queueSpecificValue, (dispatch_function_t)CFRelease);
dispatch_async(queueA, ^{
dispatch_block_t block = ^{
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
NSLog(@"执行当前任务的队列是:%@",retrievedValue);
};
CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
if (retrievedValue) { //如果当前队列是queueA,则执行block
block();
} else { //如果当前队列不是queueA,则在queueA队列上面执行。
dispatch_sync(queueA, block);
}
});
打印结果: 执行当前任务的队列是:queueA
FMDB如何使用dispatch_queue_set_specific和dispatch_get_specific来防止死锁
static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey;
//创建串行队列,所有数据库的操作都在这个队列里
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
//标记队列
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
//检查是否是同一个队列来避免死锁的方法
- (void)inDatabase:(void (^)(FMDatabase *db))block {
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
}
Operation
NSOperation简介
NSOperation
、NSOperationQueue
是苹果提供给我们的一套多线程解决方案,实际上NSOperation
、NSOperationQueue
是基于GCD更高的一层封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
NSOperation
是一个抽象类,只能使用它的子类来进行操作:
(1)NSInvocationOperation
(2)NSBlockOperation
(3)自定义
NSOperation
调用start
方法即可开始执行操作,NSOperation
对象默认按同步方式执行,也就是在调用start
方法所处的线程中直接执行。
NSOperation
对象的- (BOOL)isConcurrent;
方法会返回当前操作相对于调用start
方法的线程是同步还是异步执行,默认返回是NO,表示操作与调用线程同步执行。
NSOperationQueue简介
NSOperationQueue
是并发执行NSOperation
对象的,如果你想让NSOperation
对象串行执行的话,就需要使用NSOperation
的对象依赖特性,通过addDependency
将自己与另外一个NSOperation
对象进行关联,这样就达到了串行执行NSOperation
对象的目的。
NSOperation的特点
-
(1)添加任务依赖,控制执行顺序
通过NSOperation
的addDependency
和removeDependency
来实现,这种特点是GCD和NSThread所不具备的。 -
(2)控制任务执行的状态
isExecuteing
: 返回true表示正在执行
isFinished
:返回YES表示操作执行成功或者被取消了,NSOperationQueue
只有当它管理的所有操作的isFinished
属性全标为YES以后操作才停止出列,也就是队列停止运行,所以正确实现这个方法对于避免死锁很关键。
isCancelled
:返回true表示已经取消
-
(3)控制最大并发量
通过NSOperationQueue
的maxConcurrentOperationCount
来进行控制。 -
(4)可以简单的取消任务的执行
operation.cancel()
我们可以控制NSOperation的哪些状态
-
(1)
isReady
当前任务是否处于就绪状态 -
(2)
isExecuting
当前任务是否处于正在执行中的状态 -
(3)
isFinished
当前任务是否已执行完成 -
(4)
isCancelled
当前任务是否已取消
状态控制
如果只重写了main方法,底层控制变更任务执行完成状态,以及任务退出。
如果重写了start方法,需要自行控制任务状态
系统是怎样移除一个isFinished=YES的NSOperation的?
通过KVO
自定义Operation对象
如果NSInvocationOperation
对象和NSBlockOperation
对象不能满足我们的需求时,我们可以自己写一个类去继承自NSOperation
,然后实现我们的需求。
在自定义Operation
对象时,可以分为两种情况:
(1)非并发执行任务的Operation对象。
(2)并发执行任务的Operation对象
自定义非并发执行任务的Operation对象
主要步骤:
(1)自定义初始化方法:初始化一些属性。
(2)重写main方法:该方法就是处理主要任务的地方,你需要执行的任务都在这里。
class SerialOperation:Operation {
var url:URL?
init(url:URL) {
self.url = url
}
override func main() {
if isCancelled {
self.url = nil
return
}
guard let url = self.url else {
return
}
var dataTask:URLSessionDataTask? = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil {
if isCancelled {
self.url = nil
return
}
do {
if let data = data {
let dict = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! [String : Any]
print(dict)
}
}catch {
print(error.localizedDescription)
}
}else{
print(error!.localizedDescription)
}
}
if isCancelled {
self.url = nil
dataTask = nil
return
}
dataTask?.resume()
}
}
当我们调用start
方法时,就会执行main
方法里面的逻辑,之所以说是非并发是因为它一般在当前线程执行任务。
如果你在主线程调用它,它在主线程中执行,如果你在子线程调用它,它在子线程中执行。
如果在子线程中使用非并发自定义Operation
对象,那么main
方法中的内容应该使用autoreleasepool{}
包起来。因为在子线程中没有自动释放池,一些资源没法被回收,所以需要加一个自动释放池,如果在主线程中就不需要了。
自定义并发Operation对象
自定义并发Operation
对象其主要实现的就是让任务在当前线程以外的线程执行。
主要步骤:
(1)自定义init
方法:初始化一些属性
(2)重写start
方法:该方法是自定义并发Operation
对象必须要重写父类的一个方法,通常就在这个方法中创建子线程,让任务运行在当前线程之外的线程中,达到并发异步执行任务的目的,所以这个方法中绝对不能调用父类的start
方法。
(3)重写main
方法:该方法在自定义并发Operation
对象中,不是必须要实现的,在start
方法中就可以完成所有的事情,包括创建线程、配置执行环境以及任务逻辑,但是还是建议将任务相关的逻辑代码写在该方法中,让start
方法只负责执行环境的设置。
(4)重写executing:
用来表示Operation对象是否正在执行。
(5)重写finished:
用来表示Operation对象是否执行完成
(6)重写concurrent:用来表示Operation对象是否是并发状态。
因为并发异步执行的Operation
对象并不会阻塞主线程,所以使用它的对象需要知道它的执行情况和状态,所以这三个状态是必须要设置的。
class ConcurrentOperation:Operation {
var url:URL?
private var isConFinished:Bool
private var isConExecuting:Bool
override var isConcurrent: Bool {
get {
return true
}
}
override var isFinished: Bool {
get {
return self.isConFinished
}
}
override var isExecuting: Bool {
get {
return self.isConExecuting
}
}
init(url:URL) {
self.url = url
self.isConFinished = false
self.isConExecuting = false
}
override func start() {
if isCancelled {
willChangeValue(forKey: "finished")
isConFinished = true
didChangeValue(forKey: "finished")
return
}else {
willChangeValue(forKey: "executing")
Thread.detachNewThreadSelector(#selector(main), toTarget: self, with: nil)
isConExecuting = true
didChangeValue(forKey: "executing")
}
}
override func main() {
autoreleasepool {
guard let url = self.url else {
return
}
if isCancelled {
self.url = nil
completeOperation()
return
}
var dataTask:URLSessionDataTask? = URLSession.shared.dataTask(with: url) { (data, response, error) in
if error == nil {
if self.isCancelled {
self.url = nil
self.completeOperation()
return
}
do {
if let data = data {
let dict = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! [String : Any]
print(dict)
self.completeOperation()
}
}catch {
print(error.localizedDescription)
}
}else{
print(error!.localizedDescription)
}
}
if isCancelled {
self.url = nil
dataTask = nil
completeOperation()
return
}
dataTask?.resume()
}
}
private func completeOperation() {
willChangeValue(forKey: "finished")
willChangeValue(forKey: "executing")
isConFinished = true
isConExecuting = false
didChangeValue(forKey: "finished")
didChangeValue(forKey: "executing")
}
}
由于NSOperation
的finished
、executing
、concurrent
这三个属性都是只读,我们无法重写它们的setter
方法,所以只能通过新建的私有属性去重写它们的getter
方法。
为了自定义的Operation
对象更像原生的NSOperation
子类,我们需要通过willChangeValueForKey
和didChangeValueForKey
方法手动为isConFinished
和isConExecuting
这两个属性生成KVO通知,将key设置为原生的finished
和executing
。
在start方法开始之初就要判断一下Operation对象是否被终止任务。
main方法中的内容要放在autoreleasepool中,解决在子线程中的内存释放问题。
如果判断出Operation对象的任务已经被终止,要及时修改isConFinished和isConExecuting属性。
NSOperation实现断点续传
typealias LCADownloaderProgressBlock = ((_ receivedSize:Int64,_ expectedSize:Int64)->Void)
typealias LCADownloaderCompletedBlock = ((_ cachePath:String?,_ error:String?)->Void)
enum LCADownloadState{
case unkown //未知
case pause //暂停
case downing //下载中
case success //成功
case failed //失败
}
let kCache = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first ?? ""
let kTmp = NSTemporaryDirectory()
class LCADownloadOperation: Operation {
var state:LCADownloadState = .unkown
//当前url
var url:URL!
//开始下载
var startDownloadBlock:((_ fileSize:Int64)->Void)?
//进度
private var progressBlock:LCADownloaderProgressBlock?
//完成
private var completedBlock:LCADownloaderCompletedBlock?
//下载完成后的缓存路径
private var cacheFilePath:String?
//文件下载时的临时路径
private var tmpFilePath:String?
//临时文件大小
private var tmpFileSize:Int64 = 0
//文件总大小
private var totalFileSize:Int64 = 0
//下载会话
private var session:URLSession?
//下载任务
private var dataTask:URLSessionDataTask?
//当前线程
private var outputStream: OutputStream?
private var isRunning:Bool = false
private var isCompleted:Bool = false
override var isExecuting: Bool {
get {
return isRunning
}
set {
willChangeValue(forKey: "isExecuting")
isRunning = newValue
didChangeValue(forKey: "isExecuting")
}
}
override var isFinished: Bool{
get {
return isCompleted
}
set {
willChangeValue(forKey: "isFinished")
isCompleted = newValue
didChangeValue(forKey: "isFinished")
}
}
override var isConcurrent: Bool {
get {
return true
}
}
init(url:URL,completedBlock:@escaping LCADownloaderCompletedBlock){
super.init()
self.url = url
self.completedBlock = completedBlock
isCompleted = false
isRunning = false
totalFileSize = 0
tmpFileSize = 0
}
override func start() {
objc_sync_enter(self)
if isCancelled {
isFinished = true
reset()
return
}
if session == nil {
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 15
session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
}
download(url: url)
isExecuting = true
objc_sync_exit(self)
}
fileprivate func reset() {
progressBlock = nil
completedBlock = nil
dataTask = nil
if session != nil {
session?.invalidateAndCancel()
session = nil
}
}
override func cancel() {
objc_sync_enter(self)
DispatchQueue.global().async {
self.cancelInterval()
}
objc_sync_exit(self)
}
@objc private func cancelInterval() {
if isFinished {return}
super.cancel()
if dataTask != nil {
dataTask?.cancel()
if isExecuting {
isExecuting = false
}
if !isFinished {
isFinished = true
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: Notification.Name.LCADownloadStopNotification, object: self)
}
}
reset()
}
private func done() {
isFinished = true
isExecuting = false
reset()
}
// 暂停了几次, 恢复几次, 才可以恢复
func resume() {
if state == .pause {
dataTask?.resume()
state = .downing
}
}
// 暂停, 暂停任务, 可以恢复, 缓存没有删除
// 恢复了几次, 暂停几次, 才可以暂停
func pause() {
if state == .downing {
dataTask?.suspend()
state = .pause
}
}
private func download(url:URL) {
// 1. 下载文件的存储
// 下载中 -> tmp + (url + MD5)
// 下载完成 -> cache + url.lastCompent
cacheFilePath = kCache.appending( "/\(url.lastPathComponent)")
tmpFilePath = kTmp.appending( "/\(url.lastPathComponent.lca_md5Str)")
// 1 首先, 判断, 本地有没有已经下载好, 已经下载完毕, 就直接返回
// 文件的位置, 文件的大小
if LCADownloadFileTool.isFileExist(path: self.cacheFilePath!) {
print("文件已经下载完毕, 直接返回相应的数据--文件的具体路径, 文件的大小")
let fileSize = LCADownloadFileTool.fileSize(path: self.cacheFilePath!)
DispatchQueue.main.async {
self.startDownloadBlock?(fileSize)
}
state = .success
DispatchQueue.main.async {
self.completedBlock?(self.cacheFilePath!,nil)
self.done()
}
return
}
// 验证: 如果当前任务不存在 -> 开启任务
if let originRequestUrl = self.dataTask?.originalRequest?.url,originRequestUrl == url {
// 任务存在 -> 状态
// 状态 -> 正在下载 返回
if state == .downing {
return
}
// 状态 -> 暂停 = 恢复
if state == .pause {
resume()
return
}
//取消重新下载 == 失败
}
//读取本地的缓存大小
tmpFileSize = LCADownloadFileTool.fileSize(path: self.tmpFilePath!)
download(url: self.url, offset: tmpFileSize)
}
private func download(url:URL,offset:Int64) {
/*
bytes = 0-1000 表示下载0~1000的数据
bytes = 0- 表示从0开始下载直到下载完毕
bytes = 100- 表示从100开始下载直到下载完毕
*/
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 0)
request.setValue(String(format: "bytes=%lld-", offset), forHTTPHeaderField: "Range")
dataTask = session?.dataTask(with: request)
dataTask?.resume()
if dataTask != nil {
DispatchQueue.main.async {
NotificationCenter.default.post(name: NSNotification.Name.LCADownloadStartNotification, object: self)
}
}else {
DispatchQueue.main.async {
self.completedBlock?(nil,"创建人物失败")
}
}
}
}
/*
1.断点续传的原理
实现断点续传的功能,通常需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段
在HTTP1.1协议中定义了断点续传的相关的HTTP的Range和Content-Range字段,一个最简单的断点续传实现大概如下:
1.客户端下载一个1024K的文件,已经下载了其中512k
2.网络中断,客户端请求续传,因此需要在HTTP头中声明本次需要续传的片段:Range:bytes=512000-,这个头用来通知服务端从文件的512k位置开始传输文件
3.服务端收到断点续传请求,从文件的512k位置开始传输,并且在HTTP头文件中增加:Content-Range:bytes 512000-/1024000,并且此时服务器返回的HTTP状态码应该是206,而不是200
*/
extension LCADownloadOperation : URLSessionDataDelegate {
// 当发送的请求, 第一次接受到响应的时候调用,
//
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if let httpResponse = response as? HTTPURLResponse {
/*
Content-Length用于描述HTTP消息实体的传输长度,在HTTP协议中,消息实体长度和消息实体的传输长度是有区别的,
比如说在gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。
简单来说,content-length就是被下载文件的字节数。
*/
totalFileSize = Int64(httpResponse.allHeaderFields["Content-Length"] as! String) ?? -1
if let rangeStr = httpResponse.allHeaderFields["Content-Range"] as? String,let fileSizeStr = rangeStr.components(separatedBy: "/").last,let fileSize = Int64(fileSizeStr){
totalFileSize = fileSize
}
tmpFileSize = LCADownloadFileTool.fileSize(path: self.tmpFilePath!)
DispatchQueue.main.async {
self.startDownloadBlock?(self.totalFileSize)
}
// 判断, 本地的缓存大小 与 文件的总大小
// 缓存大小 == 文件的总大小 下载完成 -> 移动到下载完成的文件夹
if tmpFileSize == totalFileSize {
print("文件下载完成,移动数据")
_ = LCADownloadFileTool.moveFile(fromPath: self.tmpFilePath!, toPath: self.cacheFilePath!)
state = .success
completionHandler(.cancel)
return
}
if tmpFileSize > totalFileSize {
print("缓存有问题,删除缓存,重新下载")
//删除缓存
_ = LCADownloadFileTool.removeFile(path: self.tmpFilePath!)
//取消请求
completionHandler(.cancel)
//重新发送请求
download(url: url, offset: 0)
return
}
//继续接受数据
print("继续接受数据")
state = .downing
outputStream = OutputStream(toFileAtPath: self.tmpFilePath!, append: true)
outputStream?.open()
completionHandler(.allow)
}
}
//接收数据的时候调用
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
tmpFileSize += Int64(data.count)
let pt = (data as NSData).bytes.assumingMemoryBound(to: UInt8.self)
self.outputStream?.write(pt, maxLength: data.count)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.outputStream?.close()
self.outputStream = nil
if error == nil {
print((task.response as! HTTPURLResponse).statusCode)
if self.tmpFileSize == self.totalFileSize {
print("下载完毕")
}else {
print(self.tmpFileSize)
print("下载出错")
print(self.totalFileSize)
}
_ = LCADownloadFileTool.moveFile(fromPath: self.tmpFilePath!, toPath: self.cacheFilePath!)
state = .success
objc_sync_enter(self)
DispatchQueue.main.async {
self.completedBlock?(self.cacheFilePath!,nil)
self.done()
}
bjc_sync_exit(self)
}else {
print("有错误")
state = .failed
DispatchQueue.main.async {
self.completedBlock?(nil,error?.localizedDescription)
self.done()
}
}
}
}
extension Notification.Name {
static var LCADownloadStopNotification = Notification.Name("LCADownloadStopNotification")
static var LCADownloadStartNotification = Notification.Name("LCADownloadStartNotification")
}
class LCADownloadManager {
static let shared = LCADownloadManager()
private var downloadQueue:OperationQueue?
private init() {
downloadQueue = OperationQueue()
downloadQueue?.maxConcurrentOperationCount = 3
downloadQueue?.name = "com.lichangan.downloader"
}
deinit {
downloadQueue?.cancelAllOperations()
}
func download(url:URL,completedBlock:@escaping LCADownloaderCompletedBlock)->LCADownloadOperation{
let operation = LCADownloadOperation(url: url, completedBlock: completedBlock)
if canAdd(operaion: operation) {
downloadQueue?.addOperation(operation)
}
return operation
}
//暂停
func pause(url:URL){
let downloadOperation = getDownloadOperation(url: url)
if downloadOperation != nil {
downloadOperation?.pause()
}
}
//取消
func cancel(url:URL) {
let downloadOperation = getDownloadOperation(url: url)
if downloadOperation != nil {
downloadOperation?.cancel()
}
}
//取消并且清理掉缓存的临时文件
func cancelAndClearCache(url:URL){
cancel(url: url)
clearTmpCache(url: url)
}
//暂停所有
func pauseAll() {
self.downloadQueue?.operations.forEach({ (op) in
self.pause(url: (op as! LCADownloadOperation).url)
})
}
//移除所有
func removeAll() {
self.downloadQueue?.operations.forEach({ (op) in
self.cancelAndClearCache(url: (op as! LCADownloadOperation).url)
})
}
//获取临时文件的大小
func tmpCacheSize(url:URL)->Int64{
let fileMD5 = url.absoluteString.lca_md5Str()
let tmpPath = kTmp.appending("/\(fileMD5)")
return LCADownloadFileTool.fileSize(path: tmpPath)
}
//获取缓存大小
func cacheSize(url:URL)->Int64 {
let file = url.absoluteString
let tmpPath = kTmp.appending("/\(file)")
return LCADownloadFileTool.fileSize(path: tmpPath)
}
//清理缓存
func clearCache(url:URL){
let cachePath = kCache.appending("/\(url.absoluteString)")
_ = LCADownloadFileTool.removeFile(path: cachePath)
}
func clearTmpCache(url:URL){
let cachePath = kTmp.appending("/\(url.absoluteString.lca_md5Str())")
_ = LCADownloadFileTool.removeFile(path: cachePath)
}
//根据URL获取operation
private func getDownloadOperation(url:URL)->LCADownloadOperation?{
if let queue = downloadQueue {
for operation in queue.operations {
if (operation as! LCADownloadOperation).url.absoluteString == url.absoluteString {
return operation as? LCADownloadOperation
}
}
}
return nil
}
private func canAdd(operaion:LCADownloadOperation)->Bool{
if downloadQueue!.operations.count > 0 {
for dataOperation in downloadQueue!.operations {
if (dataOperation as! LCADownloadOperation).url.absoluteString == operaion.url.absoluteString {
return false
}
}
}
return true
}
}
NSThread
多线程与死锁
多线程的基本概念
-
同步
在发出一个同步调用时,没有得到结果之前,该调用不返回。 -
异步
在发出一个异步调用时,调用者不会立刻得到结果,调用会立刻返回。 -
串行
任务串行执行就是每次只有一个任务被执行,任务是一个接着一个被执行的。 -
并行
并行是你有同时处理多个任务的能力 -
并发
并发是指具有处理多个任务的能力,但是不一定是同时。这些任务可能在单核CPU上轮流切换着执行,也可能是在多核CPU上以真正的并行方式来运行
并发和并行都可以是多个线程,如果多个线程在多个CPU上被同时执行,那就是并行;如果是多个线程被一个CPU轮流切换着执行,那就是并发。
-
阻塞调用
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回 -
非阻塞调用
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
同步的定义和阻塞很像,但是同步调用的时候,线程不一定阻塞。
根据上面两组概念,可以有四种组合:
(1)同步阻塞调用:得不到结果不返回,线程进入阻塞态等待
(2)同步非阻塞调用:得不到结果不返回,线程不阻塞,CPU一直在运行。
(3)异步阻塞调用:开辟新线程,让新线程阻塞起来等待结果,自己不阻塞。
(4)异步非阻塞调用:开辟新线程,新线程一直在运行,直到得出结果。 -
线程
线程是组成进程的子单元,操作系统的调度器可以对线程进行单独的调度。多线程可以在单核CPU上同时(或者至少看做同时)运行。操作系统将小的时间分配给每一个线程,这样就能够让用户感觉到有多个任务在同时执行。如果CPU是多核的,那么线程就可以真正的以并发方式被执行,从而减少了完成某项操作所需要的总时间。 -
互斥锁
互斥访问的意思就是同一时刻,只允许一个线程访问某个特定资源。为了保证这一点,每个希望访问共享资源的线程,首先需要获得一个共享资源的互斥锁,一旦某个线程对资源完成了操作,就释放掉这个互斥锁,这样别的线程就有机会访问该共享资源了。
在资源上的加锁会引发一定的性能代价。另外,在获取锁的时候,线程有时候需要等待,因为可能其它的线程已经获取过资源的锁了。这种情况下,线程会进入休眠状态。当其它线程释放掉相关资源的锁时,休眠的线程会得到通知。所有这些相关操作都是非常昂贵且复杂的。
在使用锁时,有一个东西需要权衡:获取和释放锁是要带来开销的,因此你需要确保你不会频繁地获取和释放锁。同时,如果你获取锁之后要执行一大段代码,这将带来锁竞争的风险:其它线程可能必须等待获取资源锁而无法工作。
并发编程中遇到的问题
在并发编程中,一般会面对这样的三个问题:数据竞争、死锁问题、优先倒置。
竞态条件
并发编程中许多问题的根源就是在多线程中访问共享资源。 当两个线程在同一时间操作同一个变量时,对数据的读取和修改产生了竞争,从而导致数据结果不确定的情况。
假设现在有一个整型计数器,我们有两个并行线程A和B同时增加计数器的值,现在线程A和B都从内存中读取了计数器的值,假设为17,然后线程A将计数器的值加1,并将结果18写会到内存中。同时,线程B也将计数器的值加1,并将结果18写回到内存中。实际上,此时计数器的值已经被破坏掉了,因为计数器的值17被加1了两次,而它的值却是18。
这个问题被叫做竞态条件,在多线程里面访问一个共享的资源,如果没有一种机制来确保在线程 A 结束访问一个共享资源之前,线程 B 就不会开始访问该共享资源的话,资源竞争的问题就总是会发生。为了防止出现这样的问题,多线程需要一种同步策略来解决该问题。
常用的同步策略有线程锁、状态位、原子操作。线程锁较为简单粗暴,简单的说当一个线程在操作变量时会挂上一把互斥锁,如果另一个线程要操作该变量,它就得先获得这把锁,但是锁只有一个,必须等第一个线程释放互斥锁后,才可以被其他线程获取,所以这样就解决了资源竞争的问题。状态位策略是通过线程或任务的执行情况生成一个状态,这个状态即像门卫又像协管员,一是阻止线程进行,二是以合适的执行顺序安排协调各个任务。第三个策略则是原子操作,相对前两个策略要更轻量级一些,它能通过硬件指令保证变量在更新完成之后才能被其他线程访问。
var num = 0
DispatchQueue.global.async {
for _ in 1...10000 {
num += 1
}
}
for _ in 1...10000 {
num += 1
}
for _ in 1 ... 100000 {
print("waiting")
}
print(num)
最后的计算结果num很可能小于20000,因为其操作为非原子操作,在上述两个线程对num进行读写时其值会随着进程执行顺序的不同而产生不同的结果。当多个线程同时访问共享的数据时,会发生争用情况,第一个线程读取改变了一个变量的值,第二个线程也读取改变了这个变量的值,两个线程同时操作了该变量,此时他们会发生竞争来看哪个线程会最后写入这个变量,最后被写入的值将会被保留下来。比如说这里子线程计算到了num=9000,突然被主线程访问num,写入num的值为10,这个时候num就变成了10,当子线程再次获取num时,num已经是10了
死锁
互斥锁解决了竞态条件,但是引入死锁的问题。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡主。
你在线程之间共享的资源越多,你使用的锁也就越多,同时程序被死锁的概率也会变大。
Operation相互依赖造成死锁
let operationA = Operation()
let operationB = Operation()
operationA.addDependency(operationB)
operationB.addDependency(oprerationA)
在同一个串行队列中进行异步、同步嵌套
serialQueue.async{ //异步
serialQueue.sync{ //同步
}
}
因为串行队列一次只能执行一个任务,所以首先它会把异步block中的任务派发执行,当进入block中时,同步操作意味着阻塞当前队列。而此时外部block正在等待内部的block操作完成,而内部的block又阻塞其操作完成,即block内部在等待外部block操作完成。所以串行队列在自己等待自己释放资源造成死锁。这也提醒了我们,千万不要在主线程中用同步操作。
优先级反转
优先级反转是指程序在运行时,低优先级的任务阻塞了高优先级的任务,有效的反转了任务的优先级。高优先级和低优先级的任务之间共享资源时,就可能发生优先级反转。如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,就会阻塞当前线程,等待低优先级的线程释放所资源,从而优于迫使高优先级的线程等待低优先级的线程,这就叫做优先级反转。
当低优先级的任务获得了共享资源的锁时,该任务应该迅速完成,并释放掉锁,这样高优先级的任务就可以在没有明显延时的情况下继续执行。然而高优先级任务会在低优先级的任务持有锁的期间被阻塞。
解决这个问题的方法,通常就是不要使用不同的优先级。
var highPriorityQueue = DispatchQueue.global(qos: .userInitiated)
var lowPriorityQueue = DispatchQueue.global(qos: .utility)
let semaphore = DispatchSemaphore(value: 1) //锁对象
lowPriorityQueue.async {
semaphore.wait() //获取同步一个锁对象
for i in 0 ... 10 {
print(i)
}
semaphore.signal()
}
highPriorityQueue.async {
semaphore.wait() //获取同步一个锁对象
for i in 11 ... 20 {
print(i)
}
semaphore.signal()
}
上述的代码如果没有semaphore,高优先级的highPriorityQueue会优先执行,所以程序会优先打印11到20。而加了semaphore之后,低优先级的lowPriorityQueue会先挂起semaphore,高优先级的highPriorityQueue就只有等semaphore被释放才能再执行打印。
如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,就会阻塞当前线程,等待低优先级的线程释放所资源,从而优于迫使高优先级的线程等待低优先级的线程,这就叫做优先倒置。
iOS中的锁
@synchronized
一般在创建单例对象的时候使用,保证在多线程情况创建对象是唯一的。
平时使用最多也是性能最差的一个,适用线程不多,任务量不大的多线程加锁。
@synchronized(self) {
//需要执行的代码块
}
Swift写法:
objc_sync_enter(self)
//需要执行的代码块
objc_sync_exit(self)
//线程1
DispatchQueue.global().async {
objc_sync_enter(self)
print("线程1同步操作开始")
sleep(3)
print("线程1同步操作结束")
objc_sync_exit(self)
}
线程2
DispatchQueue.global().async {
sleep(1)
objc_sync_enter(self)
print("线程2同步操作开始")
objc_sync_exit(self)
}
打印结果:线程1同步操作开始、线程1同步操作结束、线程2同步操作开始。
@synchronized(self) 指令使用的 self 为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的 self 改成其它对象,线程2就不会被阻塞。
DispatchQueue.global().async {
objc_sync_enter(self)
print("线程1同步操作开始")
sleep(3)
print("线程1同步操作结束")
objc_sync_exit(self)
}
let obj = "test" //自定义锁对象
DispatchQueue.global().async {
sleep(1)
objc_sync_enter(obj)
print("线程2同步操作开始")
objc_sync_exit(obj)
}
打印结果:线程1同步操作开始、线程2同步操作开始、线程1同步操作结束。
@synchronized 指令实现锁的优点就是我们不需要在代码中显式的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized 块会隐式的添加一个异常处理来保护代码,该处理会在异常抛出的时候自动的释放互斥锁。所以如果不想让隐式的异常处理例程带来额外的开销,你可以考虑使用锁对象。
atomic
在 Objective-C 中将属性以 atomic 的形式来声明,就能支持互斥锁了。事实上在默认情况下,属性就是 atomic 的。将一个属性声明为 atomic 表示每次访问该属性都会进行隐式的加锁和解锁操作。虽然最稳的做法就是将所有的属性都声明为 atomic,但是加解锁这也会付出一定的代价。
atomic所说的线程安全只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的。
@property(atomic) NSMutableArray* array
self.array = [NSMutableArray array];
如果我们给属性赋值,可以通过atomic关键字来保证它的线程安全。
[self.array addObject:obj];
如果通过这种方式使用则不会保证线程安全的
OSSpinLock 自旋锁
自旋锁使用一个忙等循环(busy loop)不断的检查锁是否被释放,自旋锁不会引起调用者休眠。自旋锁在等待较少的情况下很高效,但是在等待较多的情况下非常浪费 CPU 时间。使用不当,会使CPU使用率极高。
OSSpinLock 自旋锁,性能最高的锁。它的缺点是当等待时会消耗大量 CPU 资源,不太适用于较长时间的任务。
YY大神在博客 不再安全的 OSSpinLock 中说明了OSSpinLock已经不再安全,暂不建议使用。
iOS 10 之后,苹果给出了解决方案,就是用 os_unfair_lock 代替 OSSpinLock。
var lock = os_unfair_lock(); //创建锁对象
DispatchQueue.global().async {
os_unfair_lock_lock(&lock)
print("线程1同步操作开始")
sleep(3)
print("线程1同步操作结束")
os_unfair_lock_unlock(&lock)
}
DispatchQueue.global().async {
sleep(1)
os_unfair_lock_lock(&lock)
print("线程2同步操作开始")
os_unfair_lock_unlock(&lock)
}
打印结果:线程1同步操作开始、线程1同步操作结束、线程2同步操作开始。
为什么OSSpinLock不在安全了
现在的iOS中系统维护了5中不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。
let highPriorityQueue = DispatchQueue.global(qos: .userInitiated)
let lowPriorityQueue = DispatchQueue.global(qos: .utility)
var lock = OS_SPINLOCK_INIT //自旋锁
lowPriorityQueue.async {
OSSpinLockLock(&lock) //加锁
for i in 0 ... 10 {
print(i)
}
OSSpinLockUnlock(&lock) //解锁
}
highPriorityQueue.async {
OSSpinLockLock(&lock) //获取同一个锁对象
for i in 11 ... 20 {
print(i)
}
OSSpinLockUnlock(&lock)
}
打印结果:1-20,发生优先级倒置。
自旋锁和互斥锁的区别
当一个线程尝试获取互斥锁,如果互斥锁已经被占用则线程会被挂起进入休眠状态,直到被唤醒。
当一个线程尝试获取自旋锁,如果自旋锁已经被占用,则该线程会一直循环等待并反复检查锁是否可用,直到锁可用时才会退出。
互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。
NSLock 互斥锁
NSLock 中实现了一个简单的互斥锁。通过 NSLocking 协议定义了 lock 和 unlock 方法。一般用来解决线程同步问题。
-(void)methodA{
[lock lock]; //加锁
[self methodB];
[lock unlock];//解锁
}
- (void) methodB{
[lock lock];
// 操作逻辑
[lock unlock];
}
会导致死锁。调用methodA时,会加锁,然后开始调用methodB,但是这个锁已经被一个线程持有,而此时再次methodB里面的加锁为等待其他线程释放锁,同时阻塞当前线程。而methodA又等待methodB执行完成,才能继续执行。这就形成了相互等待,导致死锁。可以通过递归锁。
NSRecursiveLock 递归锁
递归锁可以在一个线程已经持有这个锁的情况下,在后面的代码中获取多次,在递归函数和调用多个需要顺序检查同一个锁的函数时,需要用到这种锁。递归锁和基本锁不能共用。
性能不错,使用场景限制于递归。
-(void)methodA{
[recursiveLock lock]; //加锁
[self methodB];
[recursiveLock unlock];//解锁
}
- (void) methodB{
[recursiveLock lock];
// 操作逻辑
[recursiveLock unlock];
}
dispatch_semaphore_t 信号量
dispatch_semaphore_t GCD中信号量,也可以解决资源抢占问题,支持信号通知和信号等待。每当发送一个信号通知,则信号量 +1;每当发送一个等待信号时信号量 -1,;如果信号量为 0 则信号会处于等待状态,直到信号量大于 0 开始执行。
用信号来做加锁,性能很高和 OSSpinLock 差不多。
var highPriorityQueue = DispatchQueue.global(qos: .userInitiated)
var lowPriorityQueue = DispatchQueue.global(qos: .utility)
let semaphore = DispatchSemaphore(value: 1) //锁对象
lowPriorityQueue.async {
semaphore.wait() //获取同步一个锁对象,进行加锁
for i in 0 ... 10 {
print(i)
}
semaphore.signal() //解锁
}
highPriorityQueue.async {
semaphore.wait() //获取同步一个锁对象,进行加锁
for i in 11 ... 20 {
print(i)
}
semaphore.signal()
}
导致优先级倒置。
iOS中多线程的解决方案
在iOS开发中,基本有3种方式实现多线程:
- (1)NSThread可以最大限度的掌控每一个线程的生命周期。但是同时也需要开发者手动管理所有的线程活动,比如创建、同步、暂停、取消等等,其中手动加锁操作挑战性很高。总体使用场景很小,基本是造轮子或是测试时使用。
- (2)GCD是Apple推荐的方式,它将线程管理交给了系统,用的是dispatch queue的队列。开发者只要定义每个线程要执行的工作即可。所有的工作都是先进先出,每一个block运转速度极快(纳秒级别)。使用场景主要是为了追求高效处理大量并发数据,如图片异步加载、网络请求等。
- (3)Operation与GCD类似。虽然是OperationQueue队列实现,但是它并不局限于先进先出的队列操作。它提供了多个接口可以实现暂停、继续、终止、优先顺序、依赖等复杂操作,比GCD更加灵活。应用场景最广,效率上每个Operation处理速度较快(毫秒级别),几乎所有的基本线程操作都可以实现。
参考文章
并发编程:API 及挑战
GCD信号量-dispatch_semaphore_t
读 Concurrency Programming Guide 笔记(三)