iOS 多线程

作者: lazy_boy_coder | 来源:发表于2017-12-18 22:06 被阅读0次

前言

成长总是在不断的受挫中,反思,改进,最后成长

同步 vs 异步

同步和异步的维度是线程,区别是在当前执行的任务中,是否会阻塞当前的线程,如果是同步的会阻塞当前线程。异步的话,不会阻塞当前线程,他会开辟一个新的线程来执行该任务。
这么说,线程是用来执行任务的,队列是通过管理线程,来决定任务的执行方式。

NSOperation vs GCD

  • GCD是苹果基于c语言构成的API,而NSOperation是GCD的封装
  • 在NSOperationQueue,我们可以随时取消已经设定要准备执行的任务(已经开始的任务就无法阻止),而GCD停止准备执行的任务是比较困难的,没有NSOperationQueue方便
  • NSOperation 能够方便的设置依赖关系,我们可以让一个NSOperation依赖另外一个NSOperation,这样的话,尽管两个NSOperation在同一个队列中,但前者直到后者执行完毕后再执行

NSOperation

NSOperation是OC层面的对外提供的面向对象的线程管理类,是基于GCD的封装。你定义想要执行的任务,他来负责调度和执行这些任务,它管理了线程,并且使线程更加高效。

  • 管理线程,负责线程的创建和销毁,通过配置队列,让任务按照你想要的方式执行。
NSOperationQueue + NSOperation

NSOperation 任务,NSOperationQueue 队列,创建任务添加到队列中。队列有来决定任务的执行方式:

  • 并发执行,串行执行
  • 可以同时执行的并发数
  • 暂停任务,开始任务
NSBlockOperation *operation = [NSBlockOperation 
   blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];
    [operation start];

上面代码的意思是把operation任务添加到线程中执行。
NSBlockOperation 是一个继承NSOperation的类

addExecutionBlock

当然一个任务也不一定是一个线程在执行,我们可以通过addExecutionBlock给该任务添加多个线程来执行,当然苹果默认了addExecutionBlock添加到任务中的线程是异步的我们可以通过

if ([operation isConcurrent]) {
        NSLog(@"并发");
    } else {
        NSLog(@"非并发");
    }

来查看是非并发还是不是并发的

NSBlockOperation *operation = [NSBlockOperation 
blockOperationWithBlock:^{
        NSLog(@"%@",[NSThread currentThread]);
    }];

    // 添加多个block
    for (NSInteger i = 0; i < 5; i++) {
        [operation addExecutionBlock:^{
            NSLog(@"第%ld次: %@",(long)i, [NSThread currentThread]);
        }];
    }
    [operation start];
iOSThread[8646:497795] 第0次: <NSThread: 0x60000026dc40>{number = 4, name = (null)}
iOSThread[8646:497794] <NSThread: 0x60400026d840>{number = 3, name = (null)}
iOSThread[8646:497793] 第2次: <NSThread: 0x60400026db40>{number = 5, name = (null)}
iOSThread[8646:497585] 第1次: <NSThread: 0x604000068300>{number = 1, name = main}
iOSThread[8646:497793] 第4次: <NSThread: 0x60400026db40>{number = 5, name = (null)}
iOSThread[8646:497794] 第3次: <NSThread: 0x60400026d840>{number = 3, name = (null)}

可以看到addExecutionBlock,可以为该任务添加并发线程。从上面可以看出最大并发数除了主线程为还有4个,并且会把block优先分配给主线程,当主线程不在空闲时,才会选择分配到其他线程来执行。

设置代码优先级:

NSBlockOperation *downloadPic = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1 ----- %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2.0]; // 模拟下载操作
    }];
    [downloadPic setQueuePriority:NSOperationQueuePriorityVeryLow]; // 设置在队列中的优先级,较低

    NSBlockOperation *downloadMusic = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2 ----- %@",[NSThread currentThread]);
        [NSThread sleepForTimeInterval:2.0f]; // 模拟下载操作
    }];
    [downloadMusic setQueuePriority:NSOperationQueuePriorityVeryHigh];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:downloadPic];
    [queue addOperation:downloadMusic];

打印代码:

iOSThread[9549:632233] 1 ----- <NSThread: 0x604000461040>{number = 3, name = (null)}
iOSThread[9549:632235] 2 ----- <NSThread: 0x60000026c240>{number = 4, name = (null)}

可以看出,队列中的任务不需要调用start来执行。而且,添加到队列中的任务在设置优先级之后,并不一定会优先执行。对于添加到 queue的Operations,执行顺序首先由已入队列的operations是否准备好,然后在根据所有operations的相对优先级确定。是否准备好由对象间的依赖关系确定,优先级等级则是operations对象本身的一个属性。默认所有operation都拥有"普通优先级",不过可以通过设置setQueuePriority方法,来降低或提升operation对象的优先级。优先级只能应用于相同queue中的operations。队列中执行任务,需要先满足依赖关系,在根据优先级来执行。

在执行的任务当中添加依赖关系
 // 添加依赖
 //1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下载图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"打水印   - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"上传图片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//4.设置依赖
[operation2 addDependency:operation1];      //任务二依赖任务一
[operation3 addDependency:operation2];      //任务三依赖任务二

//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

队列可以有多个,任务是在加入到队列中执行的,队列中任务的执行方式是由队列来决定的,是并行队列,还是串行队列。队列与队列中的执行方式是并行的,每个队列管理这自己创建的线程。

NSInvocationOperation

这个类是继承自抽象类NSOperation的

NSInvocationOperation *operation = [[NSInvocationOperation 
alloc] initWithTarget:self selector:@selector(demo) object:nil];
    [operation start];

从打印结果上来看,这个类的执行是同步的,会在当前线程中执行。

GCD

GCD 提供C语言级别的线程管理调度,创建线程,销毁线程,都是由GCD来帮我们实现
GCD 提供两种队列,并发和串行队列

// 创建队列的时候可以设置该队列中任务的执行方式是串行的
DISPATCH_QUEUE_SERIAL
// 并行队列
DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t

在串行队列中,所有的任务都是以FIFO的形式执行,且当前队列中的任务执行完毕,才开始执行下一个任务。但是,相互独立的队列之间的任务的执行是可以并行的,不受另外一个队列的影响。也就是队列与队列之间任务的执行方式是独立的。
在并发队列中,任务的执行方式也是以FIFO(先进先出)的形式来执行的,只不过他不需要等待上一个任务执行结束,而且该队列会帮助我们创建新的线程来并发执行该任务。

// 主队列,串行队列
dispatch_get_main_queue()
// 全局并发队列,只能获取不能自己创建,可以为这个队列设置标识符,和对用的优先级
dispatch_get_global_queue(long identifier, unsigned long flags)
dispatch_get_main_queue()

如下面一段代码:

dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"执行任务");
    });

把该任务加入到主队列中,同步执行

dispatch_get_global_queue(long identifier, unsigned long flags)

上面我们说了,系统提供的全局并发队列我们只可以获取但是不能够创建。我们自己创建的队列都是采用与全局队列一样的优先级。如果,我们想要设置更高优先级的队列,可以通过获取全局队列的方式,设置他的优先级从而获得更高优先级的队列:

 dispatch_queue_t queue1 = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0);
// 高优先级
QOS_CLASS_USER_INITIATED
// 默认优先级
QOS_CLASS_DEFAULT
// 低优先级
QOS_CLASS_UTILITY
// 用户不会察觉的任务,使用他来处理预加载,或者不需要用户交互和对时间不敏感的任务。
QOS_CLASS_BACKGROUND
dispatch_set_target_queue(dispatch_object_t _Nonnull object, dispatch_queue_t _Nullable queue)

第一个参数为要设置优先级的queue,第二个参数是参照物,即将第一个queue的优先级和第二个queue的优先级设置一样

设置队列优先级

我们可以通过这个函数来设置我们自己创建队列的优先级,如下:

dispatch_queue_t testQueue = dispatch_queue_create("test1.com", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t serialQueue = dispatch_queue_create("test2.com", DISPATCH_QUEUE_SERIAL);

    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_set_target_queue(serialQueue, globalQueue);

    dispatch_async(testQueue, ^{
        NSLog(@"执行1");
    });

    dispatch_async(serialQueue, ^{
        NSLog(@"执行2");
    });

执行结果:

2017-12-17 18:22:16.766282+0800 iOSThread[14568:1310876] 执行2
2017-12-17 18:22:16.766282+0800 iOSThread[14568:1310875] 执行1

从执行结果上来看,确实设置后的优先级较高

把当前队列任务指派到其他队列中处理
dispatch_queue_t queue1 = dispatch_queue_create("test.1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("test.2", DISPATCH_QUEUE_CONCURRENT);

    dispatch_set_target_queue(queue2, queue1);

    dispatch_async(queue2, ^{
        for (NSInteger i = 0; i < 20; i++) {
            NSLog(@"queue2:%@, %ld", [NSThread currentThread], i);
        }
    });

    dispatch_async(queue1, ^{
        for (NSInteger i = 0; i < 20; i++) {
            NSLog(@"queue1:%@, %ld", [NSThread currentThread], i);
        }
    });

就是把当前queue2中的任务指派到queue1中去执行,从上面代码可以看出queue1是串行队列,那么queue2中的任务就会在queue1中以串行的方式执行。

dispatch_after
dispatch_time
dispatch_time(dispatch_time_t when, int64_t delta)
第一个参数可以设置当前时间: DISPATCH_TIME_NOW
第二个参数delta表示纳秒,可以直接使用NSEC_PER_SEC

那么我们可以直接设置延时任务

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"延时执行的任务");
    });

这段代码的意思是,这个函数等待到指定的时间。然后把任务异步添加到指定队列执行
既延时2s,然后把任务添加到主队列执行

dispatch_queue_set_specific & dispatch_get_specific

dispatch_queue_set_specific 在指定队列中设定一个标识
dispatch_get_specific 在当前队列中取出标识

dispatch_group

把一组任务提交到队列当中,这些队列可以不相关,然后监听这组任务的完成

dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue1 = dispatch_queue_create("test.com", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{ 
        for (int i = 0; i < 30; i++) {
            NSLog(@"group1 -%d - %@",i, [NSThread currentThread]);
        }
    });

    dispatch_group_async(group, queue1, ^{
        for (int i = 0; i < 30; i++) {
            NSLog(@"group2 - %d - %@", i, [NSThread currentThread]);
        }
    });

    //完成之后回调
    dispatch_group_notify(group, queue, ^{
        NSLog(@"完成任务 - %@",[NSThread currentThread]);
    });
dispatch_barrier_async

当在并发队列中遇到一个barrier,这个方法会阻塞queue(不是阻塞当前线程)。一直等到排在这个queue前面的任务执行完后才开始执行自己,自己执行完毕后,再会取消阻塞。使这个queue中排在它后面的任务继续执行。如果你传入的是其他queue,那么就和dispatch_async一样了

dispatch_queue_t queue = dispatch_queue_create("sdk.com", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2.f];
        NSLog(@"task 1");
    });

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2.f];
    });

    // 等待前面的都完成,在执行 barrier 后面的
    dispatch_barrier_async(queue, ^{
        NSLog(@"task 2");
        [NSThread sleepForTimeInterval:2.f];
    });

    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:3.f];
        NSLog(@"task 3");
    });

    dispatch_async(queue, ^{
        NSLog(@"task 4");
    });
2017-12-18 21:51:09.001390+0800 iOSThread[37907:2827277] task 1
2017-12-18 21:51:09.001733+0800 iOSThread[37907:2827277] task 2
2017-12-18 21:51:11.005180+0800 iOSThread[37907:2827274] task 4
2017-12-18 21:51:14.008395+0800 iOSThread[37907:2827277] task 3
dispatch_barrier_sync

这个方法的使用和上一个一样,传入自定义的并发队列DISPATCH_QUEUE_CONCURRENT,它和上一个方法一样的阻塞queue,不同的是,这个方法还会阻塞当前线程。如果,你传入的是其他的queue,那么就和 "dispatch_sync" 是一样的了。

造成死锁的几个例子
NSLog(@"之前 - %@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"sync - %@",[NSThread currentThread]);
    });
    NSLog(@"之后 - %@",[NSThread currentThread]);

这段代码的执行是,先打印第一句,然后阻塞当前线程,把block放到 main_queue中。可以main_queue中的任务会被取出来放到主线程中执行,但是主线程已经被阻塞。这样就造成了一个死锁。

另外一个例子:

dispatch_queue_t queue  = dispatch_queue_create("test.com", DISPATCH_QUEUE_SERIAL);
    NSLog(@"之前 - %@",[NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"sync之前 - %@",[NSThread currentThread]);
        dispatch_sync(queue, ^{
            NSLog(@"sync - %@",[NSThread currentThread]);
        });
        NSLog(@"sync - 之后%@",[NSThread currentThread]);
    });
    NSLog(@"之后 - %@",[NSThread currentThread]);

1.使用 DISPATCH_QUEUE_SERIAL 这个参数,创建一个串行的队列。
2.在 dispatch_async 异步执行,所以当前线程不会被阻塞,于是就有了两条线程。一条当前线程继续往下打印"之后 - ",而另外一条线程执行block中的内容打印 "sync之前 - ",这句。因为两条是并行的所以打印的先后顺序无所谓。
3.dispatch_sync同步执行,于是他把当前的线程阻塞,一直等到sync里的任务执行完才会继续往下。于是,sync就高兴的把自己Block中的任务放到queue中,可谁想queue是一个串行队列,一次执行一个任务。所以queue必须等到当前的任务执行完毕,但是queue又被阻塞了,于是就发生死锁。

相关文章

网友评论

    本文标题:iOS 多线程

    本文链接:https://www.haomeiwen.com/subject/ukhewxtx.html